You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by li...@apache.org on 2023/05/11 10:06:35 UTC

[incubator-devlake] branch main updated: feat(config-ui): improve the connection detail page (#5158)

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 c488fea71 feat(config-ui): improve the connection detail page (#5158)
c488fea71 is described below

commit c488fea7100d1ec91bdc7edd16dc52760cb84ea1
Author: 青湛 <0x...@gmail.com>
AuthorDate: Thu May 11 18:06:30 2023 +0800

    feat(config-ui): improve the connection detail page (#5158)
    
    * feat(config-ui): add global store tips
    
    * feat(config-ui): add props disabledItems in data scope
    
    * feat(config-ui): improve the connection detail page
---
 config-ui/src/layouts/base/base.tsx                | 196 ++++++++++---------
 config-ui/src/layouts/base/styled.ts               |  25 ++-
 config-ui/src/pages/connection/detail/api.ts       |   2 +
 config-ui/src/pages/connection/detail/index.tsx    |  86 ++++++++-
 config-ui/src/pages/connection/detail/styled.ts    |   5 +
 .../components/data-scope-form-2}/api.ts           |  16 +-
 .../plugins/components/data-scope-form-2/index.tsx | 215 +++++++++++++++++++++
 .../components/data-scope-form-2/styled.ts}        |  16 +-
 .../components/data-scope-miller-columns/index.tsx |  10 +-
 .../plugins/components/data-scope-search/index.tsx |   4 +-
 config-ui/src/plugins/components/index.ts          |   1 +
 .../src/plugins/register/azure/data-scope.tsx      |   9 +-
 .../src/plugins/register/bitbucket/data-scope.tsx  |   9 +-
 .../github/components/miller-columns/index.tsx     |   9 +-
 .../github/components/repo-selector/index.tsx      |   6 +-
 .../src/plugins/register/github/data-scope.tsx     |  17 +-
 .../src/plugins/register/gitlab/data-scope.tsx     |   8 +
 .../jenkins/components/miller-columns/index.tsx    |   9 +-
 .../src/plugins/register/jenkins/data-scope.tsx    |   3 +-
 .../jira/components/miller-columns/index.tsx       |   9 +-
 config-ui/src/plugins/register/jira/data-scope.tsx |  12 +-
 .../src/plugins/register/pagerduty/data-scope.tsx  |   7 +
 .../src/plugins/register/sonarqube/data-scope.tsx  |   9 +-
 config-ui/src/plugins/register/tapd/data-scope.tsx |   9 +-
 .../src/plugins/register/zentao/data-scope.tsx     |  14 +-
 config-ui/src/store/index.ts                       |   1 +
 .../jira/data-scope.tsx => store/tips/context.tsx} |  37 ++--
 config-ui/src/store/{ => tips}/index.ts            |   2 +-
 28 files changed, 593 insertions(+), 153 deletions(-)

diff --git a/config-ui/src/layouts/base/base.tsx b/config-ui/src/layouts/base/base.tsx
index 5ee3cfa43..8b5d8a069 100644
--- a/config-ui/src/layouts/base/base.tsx
+++ b/config-ui/src/layouts/base/base.tsx
@@ -22,6 +22,7 @@ import { Menu, MenuItem, Tag, Navbar, Intent, Alignment, Button } from '@bluepri
 
 import { PageLoading, Logo, ExternalLink } from '@/components';
 import { useRefreshData } from '@/hooks';
+import { TipsContextProvider, TipsContextConsumer } from '@/store';
 import { history } from '@/utils/history';
 
 import DashboardIcon from '@/images/icons/dashborad.svg';
@@ -71,99 +72,106 @@ export const BaseLayout = ({ children }: Props) => {
   }
 
   return (
-    <S.Wrapper>
-      <S.Sider>
-        <Logo />
-        <Menu className="menu">
-          {menu.map((it) => {
-            const paths = [it.path, ...(it.children ?? []).map((cit) => cit.path)];
-            const active = !!paths.find((path) => pathname.includes(path));
-            return (
-              <MenuItem
-                key={it.key}
-                className="menu-item"
-                text={it.title}
-                icon={it.icon}
-                active={active}
-                onClick={() => handlePushPath(it)}
-              >
-                {it.children?.map((cit) => (
-                  <MenuItem
-                    key={cit.key}
-                    className="sub-menu-item"
-                    text={
-                      <S.SiderMenuItem>
-                        <span>{cit.title}</span>
-                        {cit.isBeta && <Tag intent={Intent.WARNING}>beta</Tag>}
-                      </S.SiderMenuItem>
-                    }
-                    icon={cit.icon ?? <img src={cit.iconUrl} width={16} alt="" />}
-                    active={pathname.includes(cit.path)}
-                    disabled={cit.disabled}
-                    onClick={() => handlePushPath(cit)}
-                  />
-                ))}
-              </MenuItem>
-            );
-          })}
-        </Menu>
-        <div className="copyright">
-          <div>Apache 2.0 License</div>
-          <div className="version">{data.version}</div>
-        </div>
-      </S.Sider>
-      <S.Main>
-        <S.Header>
-          <Navbar.Group align={Alignment.RIGHT}>
-            <S.DashboardIcon>
-              <ExternalLink link={getGrafanaUrl()}>
-                <img src={DashboardIcon} alt="dashboards" />
-                <span>Dashboards</span>
-              </ExternalLink>
-            </S.DashboardIcon>
-            <Navbar.Divider />
-            <a href="https://devlake.apache.org/docs/Configuration/Tutorial" rel="noreferrer" target="_blank">
-              <img src={FileIcon} alt="documents" />
-              <span>Docs</span>
-            </a>
-            <Navbar.Divider />
-            <ExternalLink link="/api/swagger/index.html">
-              <img src={APIIcon} alt="api" />
-              <span>API</span>
-            </ExternalLink>
-            <Navbar.Divider />
-            <a
-              href="https://github.com/apache/incubator-devlake"
-              rel="noreferrer"
-              target="_blank"
-              className="navIconLink"
-            >
-              <img src={GitHubIcon} alt="github" />
-              <span>GitHub</span>
-            </a>
-            <Navbar.Divider />
-            <a
-              href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ"
-              rel="noreferrer"
-              target="_blank"
-            >
-              <img src={SlackIcon} alt="slack" />
-              <span>Slack</span>
-            </a>
-            {token && (
-              <>
-                <Navbar.Divider />
-                <Button small intent={Intent.NONE} onClick={handleSignOut}>
-                  Sign Out
-                </Button>
-              </>
-            )}
-          </Navbar.Group>
-        </S.Header>
-        <S.Inner>
-          <S.Content>{children}</S.Content>
-        </S.Inner>
-      </S.Main>
-    </S.Wrapper>
+    <TipsContextProvider>
+      <TipsContextConsumer>
+        {({ text }) => (
+          <S.Wrapper>
+            <S.Sider>
+              <Logo />
+              <Menu className="menu">
+                {menu.map((it) => {
+                  const paths = [it.path, ...(it.children ?? []).map((cit) => cit.path)];
+                  const active = !!paths.find((path) => pathname.includes(path));
+                  return (
+                    <MenuItem
+                      key={it.key}
+                      className="menu-item"
+                      text={it.title}
+                      icon={it.icon}
+                      active={active}
+                      onClick={() => handlePushPath(it)}
+                    >
+                      {it.children?.map((cit) => (
+                        <MenuItem
+                          key={cit.key}
+                          className="sub-menu-item"
+                          text={
+                            <S.SiderMenuItem>
+                              <span>{cit.title}</span>
+                              {cit.isBeta && <Tag intent={Intent.WARNING}>beta</Tag>}
+                            </S.SiderMenuItem>
+                          }
+                          icon={cit.icon ?? <img src={cit.iconUrl} width={16} alt="" />}
+                          active={pathname.includes(cit.path)}
+                          disabled={cit.disabled}
+                          onClick={() => handlePushPath(cit)}
+                        />
+                      ))}
+                    </MenuItem>
+                  );
+                })}
+              </Menu>
+              <div className="copyright">
+                <div>Apache 2.0 License</div>
+                <div className="version">{data.version}</div>
+              </div>
+            </S.Sider>
+            <S.Main>
+              <S.Header>
+                <Navbar.Group align={Alignment.RIGHT}>
+                  <S.DashboardIcon>
+                    <ExternalLink link={getGrafanaUrl()}>
+                      <img src={DashboardIcon} alt="dashboards" />
+                      <span>Dashboards</span>
+                    </ExternalLink>
+                  </S.DashboardIcon>
+                  <Navbar.Divider />
+                  <a href="https://devlake.apache.org/docs/Configuration/Tutorial" rel="noreferrer" target="_blank">
+                    <img src={FileIcon} alt="documents" />
+                    <span>Docs</span>
+                  </a>
+                  <Navbar.Divider />
+                  <ExternalLink link="/api/swagger/index.html">
+                    <img src={APIIcon} alt="api" />
+                    <span>API</span>
+                  </ExternalLink>
+                  <Navbar.Divider />
+                  <a
+                    href="https://github.com/apache/incubator-devlake"
+                    rel="noreferrer"
+                    target="_blank"
+                    className="navIconLink"
+                  >
+                    <img src={GitHubIcon} alt="github" />
+                    <span>GitHub</span>
+                  </a>
+                  <Navbar.Divider />
+                  <a
+                    href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ"
+                    rel="noreferrer"
+                    target="_blank"
+                  >
+                    <img src={SlackIcon} alt="slack" />
+                    <span>Slack</span>
+                  </a>
+                  {token && (
+                    <>
+                      <Navbar.Divider />
+                      <Button small intent={Intent.NONE} onClick={handleSignOut}>
+                        Sign Out
+                      </Button>
+                    </>
+                  )}
+                </Navbar.Group>
+              </S.Header>
+              <S.Inner>
+                <S.Content>{children}</S.Content>
+              </S.Inner>
+              {text && <S.Tips>{text}</S.Tips>}
+            </S.Main>
+          </S.Wrapper>
+        )}
+      </TipsContextConsumer>
+    </TipsContextProvider>
   );
 };
diff --git a/config-ui/src/layouts/base/styled.ts b/config-ui/src/layouts/base/styled.ts
index c3303e89a..f04f314ec 100644
--- a/config-ui/src/layouts/base/styled.ts
+++ b/config-ui/src/layouts/base/styled.ts
@@ -82,14 +82,6 @@ export const Sider = styled.div`
   }
 `;
 
-export const Main = styled.div`
-  display: flex;
-  flex-direction: column;
-  flex: auto;
-  height: 100vh;
-  overflow: hidden;
-`;
-
 export const Header = styled(Navbar)`
   flex: 0 0 50px;
   background-color: #f9f9fa;
@@ -110,6 +102,14 @@ export const Header = styled(Navbar)`
   }
 `;
 
+export const Main = styled.div`
+  display: flex;
+  flex-direction: column;
+  flex: auto;
+  height: 100vh;
+  overflow: hidden;
+`;
+
 export const Inner = styled.div`
   flex: auto;
   margin-top: 24px;
@@ -124,6 +124,15 @@ export const Content = styled.div`
   min-width: 900px;
 `;
 
+export const Tips = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 14px 0;
+  color: #fff;
+  background-color: #3c5088;
+`;
+
 export const SiderMenuItem = styled.div`
   display: flex;
   align-items: center;
diff --git a/config-ui/src/pages/connection/detail/api.ts b/config-ui/src/pages/connection/detail/api.ts
index 3d7df8db5..c86588cc4 100644
--- a/config-ui/src/pages/connection/detail/api.ts
+++ b/config-ui/src/pages/connection/detail/api.ts
@@ -20,3 +20,5 @@ import { request } from '@/utils';
 
 export const deleteConnection = (plugin: string, id: ID) =>
   request(`/plugins/${plugin}/connections/${id}`, { method: 'delete' });
+
+export const getDataScope = (plugin: string, id: ID) => request(`/plugins/${plugin}/connections/${id}/scopes`);
diff --git a/config-ui/src/pages/connection/detail/index.tsx b/config-ui/src/pages/connection/detail/index.tsx
index 4ce9a8dc3..743b08174 100644
--- a/config-ui/src/pages/connection/detail/index.tsx
+++ b/config-ui/src/pages/connection/detail/index.tsx
@@ -20,11 +20,12 @@ import { useState } from 'react';
 import { useParams, useHistory } from 'react-router-dom';
 import { Button, Icon, Intent } from '@blueprintjs/core';
 
-import { PageHeader, Dialog, IconButton } from '@/components';
+import { PageHeader, Dialog, IconButton, Table } from '@/components';
 import { transformEntities } from '@/config';
-import { ConnectionForm } from '@/plugins';
+import { useRefreshData } from '@/hooks';
+import { ConnectionForm, DataScopeForm2 } from '@/plugins';
 import type { ConnectionItemType } from '@/store';
-import { ConnectionContextProvider, useConnection, ConnectionStatus } from '@/store';
+import { ConnectionContextProvider, useConnection, ConnectionStatus, useTips } from '@/store';
 import { operator } from '@/utils';
 
 import * as API from './api';
@@ -36,15 +37,35 @@ interface Props {
 }
 
 const ConnectionDetail = ({ plugin, id }: Props) => {
-  const [type, setType] = useState<'deleteConnection' | 'updateConnection'>();
+  const [type, setType] = useState<'deleteConnection' | 'updateConnection' | 'createDataScope'>();
   const [operating, setOperating] = useState(false);
+  const [version, setVersion] = useState(1);
 
   const history = useHistory();
   const { connections, onRefresh, onTest } = useConnection();
+  const { setText } = useTips();
+  const { ready, data } = useRefreshData(() => API.getDataScope(plugin, id), [version]);
+
   const { unique, status, name, icon, entities } = connections.find(
     (cs) => cs.unique === `${plugin}-${id}`,
   ) as ConnectionItemType;
 
+  const handleHideDialog = () => {
+    setType(undefined);
+  };
+
+  const handleShowTips = () => {
+    setText(
+      <div>
+        <Icon icon="warning-sign" style={{ marginRight: 8 }} color="#F4BE55" />
+        <span>
+          The transformation of certain data scope has been updated. If you would like to re-transform the data in the
+          related project(s), please go to the Project page and do so.
+        </span>
+      </div>,
+    );
+  };
+
   const handleShowDeleteDialog = () => {
     setType('deleteConnection');
   };
@@ -65,12 +86,18 @@ const ConnectionDetail = ({ plugin, id }: Props) => {
   };
 
   const handleUpdate = () => {
-    setType(undefined);
     onRefresh(plugin);
+    handleHideDialog();
   };
 
-  const handleHideDialog = () => {
-    setType(undefined);
+  const handleShowCreateDataScopeDialog = () => {
+    setType('createDataScope');
+  };
+
+  const handleCreateDataScope = () => {
+    setVersion((v) => v + 1);
+    handleShowTips();
+    handleHideDialog();
   };
 
   return (
@@ -102,6 +129,25 @@ const ConnectionDetail = ({ plugin, id }: Props) => {
             </span>
           </div>
         </div>
+        <div className="action">
+          <Button intent={Intent.PRIMARY} icon="add" text="Add Data Scope" onClick={handleShowCreateDataScopeDialog} />
+        </div>
+        <Table
+          loading={!ready}
+          columns={[
+            {
+              title: 'Data Scope',
+              dataIndex: 'name',
+              key: 'name',
+            },
+          ]}
+          dataSource={data}
+          noData={{
+            text: 'Add data to this connection.',
+            btnText: 'Add Data Scope',
+            onCreate: handleShowCreateDataScopeDialog,
+          }}
+        />
       </S.Wrapper>
       {type === 'deleteConnection' && (
         <Dialog
@@ -123,9 +169,9 @@ const ConnectionDetail = ({ plugin, id }: Props) => {
       )}
       {type === 'updateConnection' && (
         <Dialog
+          isOpen
           style={{ width: 820 }}
           footer={null}
-          isOpen
           title={
             <S.DialogTitle>
               <img src={icon} alt="" />
@@ -137,6 +183,28 @@ const ConnectionDetail = ({ plugin, id }: Props) => {
           <ConnectionForm plugin={plugin} connectionId={id} onSuccess={handleUpdate} />
         </Dialog>
       )}
+      {type === 'createDataScope' && (
+        <Dialog
+          isOpen
+          style={{ width: 820 }}
+          footer={null}
+          title={
+            <S.DialogTitle>
+              <img src={icon} alt="" />
+              <span>Add Data Scope: {name}</span>
+            </S.DialogTitle>
+          }
+          onCancel={handleHideDialog}
+        >
+          <DataScopeForm2
+            plugin={plugin}
+            connectionId={id}
+            disabledScope={data}
+            onCancel={handleHideDialog}
+            onSubmit={handleCreateDataScope}
+          />
+        </Dialog>
+      )}
     </PageHeader>
   );
 };
@@ -146,7 +214,7 @@ export const ConnectionDetailPage = () => {
 
   return (
     <ConnectionContextProvider plugin={plugin}>
-      <ConnectionDetail plugin={plugin} id={id} />
+      <ConnectionDetail plugin={plugin} id={+id} />
     </ConnectionContextProvider>
   );
 };
diff --git a/config-ui/src/pages/connection/detail/styled.ts b/config-ui/src/pages/connection/detail/styled.ts
index e8b6e0212..4a9225132 100644
--- a/config-ui/src/pages/connection/detail/styled.ts
+++ b/config-ui/src/pages/connection/detail/styled.ts
@@ -33,6 +33,11 @@ export const Wrapper = styled.div`
       span 
     }
   }
+
+  .action {
+    margin-top: 36px;
+    margin-bottom: 24px;
+  }
 `;
 
 export const DialogTitle = styled.div`
diff --git a/config-ui/src/pages/connection/detail/api.ts b/config-ui/src/plugins/components/data-scope-form-2/api.ts
similarity index 58%
copy from config-ui/src/pages/connection/detail/api.ts
copy to config-ui/src/plugins/components/data-scope-form-2/api.ts
index 3d7df8db5..19abb110d 100644
--- a/config-ui/src/pages/connection/detail/api.ts
+++ b/config-ui/src/plugins/components/data-scope-form-2/api.ts
@@ -18,5 +18,17 @@
 
 import { request } from '@/utils';
 
-export const deleteConnection = (plugin: string, id: ID) =>
-  request(`/plugins/${plugin}/connections/${id}`, { method: 'delete' });
+export const getDataScope = (plugin: string, connectionId: ID, scopeId: string) =>
+  request(`/plugins/${plugin}/connections/${connectionId}/scopes/${scopeId}`);
+
+export const updateDataScope = (plugin: string, connectionId: ID, payload: any) =>
+  request(`/plugins/${plugin}/connections/${connectionId}/scopes`, {
+    method: 'put',
+    data: payload,
+  });
+
+export const updateDataScopeWithType = (plugin: string, connectionId: ID, type: string, payload: any) =>
+  request(`/plugins/${plugin}/connections/${connectionId}/${type}/scopes`, {
+    method: 'put',
+    data: payload,
+  });
diff --git a/config-ui/src/plugins/components/data-scope-form-2/index.tsx b/config-ui/src/plugins/components/data-scope-form-2/index.tsx
new file mode 100644
index 000000000..4d64dc8df
--- /dev/null
+++ b/config-ui/src/plugins/components/data-scope-form-2/index.tsx
@@ -0,0 +1,215 @@
+/*
+ * 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 { useMemo, useState } from 'react';
+import { Button, Intent } from '@blueprintjs/core';
+
+import { getPluginId } from '@/plugins';
+
+import { GitHubDataScope } from '@/plugins/register/github';
+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';
+
+import * as API from './api';
+import * as S from './styled';
+import { TapdDataScope } from '@/plugins/register/tapd';
+
+interface Props {
+  plugin: string;
+  connectionId: ID;
+  disabledScope?: any[];
+  cancelBtnProps?: {
+    text?: string;
+  };
+  submitBtnProps?: {
+    text?: string;
+  };
+  onCancel?: () => void;
+  onSubmit?: (origin: any) => void;
+}
+
+export const DataScopeForm2 = ({
+  plugin,
+  connectionId,
+  disabledScope,
+  onSubmit,
+  onCancel,
+  cancelBtnProps,
+  submitBtnProps,
+}: Props) => {
+  const [operating, setOperating] = useState(false);
+  const [scope, setScope] = useState<any>([]);
+
+  const error = useMemo(() => (!scope.length ? 'No Data Scope is Selected' : ''), [scope]);
+
+  const getDataScope = async (scope: any) => {
+    try {
+      const res = await API.getDataScope(plugin, connectionId, scope[getPluginId(plugin)]);
+      return {
+        ...scope,
+        transformationRuleId: res.transformationRuleId,
+      };
+    } catch {
+      return scope;
+    }
+  };
+
+  const handleSubmit = async () => {
+    setOperating(true);
+    try {
+      const data = await Promise.all(scope.map((sc: any) => getDataScope(sc)));
+      const res =
+        plugin === 'zentao'
+          ? [
+              ...(await API.updateDataScopeWithType(plugin, connectionId, 'product', {
+                data: data.filter((s) => s.type !== 'project'),
+              })),
+              ...(await API.updateDataScopeWithType(plugin, connectionId, 'project', {
+                data: data.filter((s) => s.type === 'project'),
+              })),
+            ]
+          : await API.updateDataScope(plugin, connectionId, {
+              data,
+            });
+
+      onSubmit?.(res);
+    } finally {
+      setOperating(false);
+    }
+  };
+
+  return (
+    <S.Wrapper>
+      {plugin === 'github' && (
+        <GitHubDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'jira' && (
+        <JiraDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'gitlab' && (
+        <GitLabDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'jenkins' && (
+        <JenkinsDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'bitbucket' && (
+        <BitbucketDataScope
+          disabledItems={disabledScope}
+          connectionId={connectionId}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'azuredevops' && (
+        <AzureDataScope
+          disabledItems={disabledScope}
+          connectionId={connectionId}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'sonarqube' && (
+        <SonarQubeDataScope
+          disabledItems={disabledScope}
+          connectionId={connectionId}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'pagerduty' && (
+        <PagerDutyDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'tapd' && (
+        <TapdDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      {plugin === 'zentao' && (
+        <ZentaoDataScope
+          connectionId={connectionId}
+          disabledItems={disabledScope}
+          selectedItems={scope}
+          onChangeItems={setScope}
+        />
+      )}
+
+      <div className="action">
+        <Button
+          outlined
+          intent={Intent.PRIMARY}
+          text="Cancel"
+          {...cancelBtnProps}
+          disabled={operating}
+          onClick={onCancel}
+        />
+        <Button
+          outlined
+          intent={Intent.PRIMARY}
+          text="Save"
+          {...submitBtnProps}
+          loading={operating}
+          disabled={!!error}
+          onClick={handleSubmit}
+        />
+      </div>
+    </S.Wrapper>
+  );
+};
diff --git a/config-ui/src/pages/connection/detail/api.ts b/config-ui/src/plugins/components/data-scope-form-2/styled.ts
similarity index 76%
copy from config-ui/src/pages/connection/detail/api.ts
copy to config-ui/src/plugins/components/data-scope-form-2/styled.ts
index 3d7df8db5..e10dd267e 100644
--- a/config-ui/src/pages/connection/detail/api.ts
+++ b/config-ui/src/plugins/components/data-scope-form-2/styled.ts
@@ -16,7 +16,17 @@
  *
  */
 
-import { request } from '@/utils';
+import styled from 'styled-components';
 
-export const deleteConnection = (plugin: string, id: ID) =>
-  request(`/plugins/${plugin}/connections/${id}`, { method: 'delete' });
+export const Wrapper = styled.div`
+  .action {
+    display: flex;
+    align-items: center;
+    justify-content: end;
+    margin-top: 36px;
+
+    .bp4-button + .bp4-button {
+      margin-left: 8px;
+    }
+  }
+`;
diff --git a/config-ui/src/plugins/components/data-scope-miller-columns/index.tsx b/config-ui/src/plugins/components/data-scope-miller-columns/index.tsx
index 3f380011c..0fe73014b 100644
--- a/config-ui/src/plugins/components/data-scope-miller-columns/index.tsx
+++ b/config-ui/src/plugins/components/data-scope-miller-columns/index.tsx
@@ -31,6 +31,7 @@ interface Props extends Pick<MillerColumnsSelectProps<ExtraType>, 'columnCount'>
   title?: string;
   plugin: string;
   connectionId: ID;
+  disabledItems?: any[];
   selectedItems?: any[];
   pageToken?: string;
   onChangeItems?: (selectedItems: any[]) => void;
@@ -40,6 +41,7 @@ export const DataScopeMillerColumns = ({
   title,
   plugin,
   connectionId,
+  disabledItems,
   selectedItems,
   onChangeItems,
   pageToken,
@@ -47,13 +49,18 @@ export const DataScopeMillerColumns = ({
 }: Props) => {
   const [items, setItems] = useState<McsItem<ExtraType>[]>([]);
   const [selectedIds, setSelectedIds] = useState<ID[]>([]);
+  const [disabledIds, setDisabledIds] = useState<ID[]>([]);
   const [loadedIds, setLoadedIds] = useState<ID[]>([]);
   const [nextTokenMap, setNextTokenMap] = useState<Record<ID, string>>({});
 
   useEffect(() => {
-    setSelectedIds((selectedItems ?? []).map((it: any) => it.id));
+    setSelectedIds((selectedItems ?? []).map((it) => it.id));
   }, [selectedItems]);
 
+  useEffect(() => {
+    setDisabledIds((disabledItems ?? []).map((it) => it.id));
+  }, [disabledItems]);
+
   const getItems = async (groupId: ID | null, currentPageToken?: string) => {
     if (!currentPageToken) {
       currentPageToken = pageToken;
@@ -122,6 +129,7 @@ export const DataScopeMillerColumns = ({
       columnHeight={300}
       renderTitle={renderTitle}
       renderLoading={renderLoading}
+      disabledIds={disabledIds}
       selectedIds={selectedIds}
       onSelectItemIds={handleChangeItems}
       {...props}
diff --git a/config-ui/src/plugins/components/data-scope-search/index.tsx b/config-ui/src/plugins/components/data-scope-search/index.tsx
index 586b2338d..3d2aa7e8a 100644
--- a/config-ui/src/plugins/components/data-scope-search/index.tsx
+++ b/config-ui/src/plugins/components/data-scope-search/index.tsx
@@ -26,11 +26,12 @@ import * as API from './api';
 interface Props {
   plugin: string;
   connectionId: ID;
+  disabledItems?: any[];
   selectedItems?: any[];
   onChangeItems?: (selectedItems: any[]) => void;
 }
 
-export const DataScopeSearch = ({ plugin, connectionId, selectedItems, onChangeItems }: Props) => {
+export const DataScopeSearch = ({ plugin, connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   const [loading, setLoading] = useState(false);
   const [items, setItems] = useState<ItemType[]>([]);
   const [search, setSearch] = useState('');
@@ -68,6 +69,7 @@ export const DataScopeSearch = ({ plugin, connectionId, selectedItems, onChangeI
       items={items}
       getKey={getKey}
       getName={getName}
+      disabledItems={disabledItems}
       selectedItems={selectedItems}
       onChangeItems={handleChangeItems}
       loading={loading}
diff --git a/config-ui/src/plugins/components/index.ts b/config-ui/src/plugins/components/index.ts
index 307482460..6f45b86b7 100644
--- a/config-ui/src/plugins/components/index.ts
+++ b/config-ui/src/plugins/components/index.ts
@@ -19,6 +19,7 @@
 export * from './connection-form';
 export * from './data-scope';
 export * from './data-scope-form';
+export * from './data-scope-form-2';
 export * from './data-scope-miller-columns';
 export * from './data-scope-search';
 export * from './transformation';
diff --git a/config-ui/src/plugins/register/azure/data-scope.tsx b/config-ui/src/plugins/register/azure/data-scope.tsx
index 41393db42..7659d7425 100644
--- a/config-ui/src/plugins/register/azure/data-scope.tsx
+++ b/config-ui/src/plugins/register/azure/data-scope.tsx
@@ -16,7 +16,7 @@
  *
  */
 
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
 
 import { DataScopeMillerColumns } from '@/plugins';
 
@@ -24,6 +24,7 @@ import type { AzureScopeType } from './types';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: AzureScopeType[];
   selectedItems: AzureScopeType[];
   onChangeItems: (selectedItems: AzureScopeType[]) => void;
 }
@@ -34,6 +35,11 @@ export const AzureDataScope = ({ connectionId, onChangeItems, ...props }: Props)
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: `${it.id}`, name: it.name, data: it })),
+    [props.disabledItems],
+  );
+
   return (
     <>
       <h4>Add Repositories by Selecting from the Directory</h4>
@@ -41,6 +47,7 @@ export const AzureDataScope = ({ connectionId, onChangeItems, ...props }: Props)
       <DataScopeMillerColumns
         plugin="azuredevops"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/plugins/register/bitbucket/data-scope.tsx b/config-ui/src/plugins/register/bitbucket/data-scope.tsx
index 9fd8a1163..d0130b213 100644
--- a/config-ui/src/plugins/register/bitbucket/data-scope.tsx
+++ b/config-ui/src/plugins/register/bitbucket/data-scope.tsx
@@ -16,7 +16,7 @@
  *
  */
 
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
 
 import { DataScopeMillerColumns } from '@/plugins';
 
@@ -24,6 +24,7 @@ import type { ScopeItemType } from './types';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
@@ -34,6 +35,11 @@ export const BitbucketDataScope = ({ connectionId, onChangeItems, ...props }: Pr
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: `${it.bitbucketId}`, name: it.name, data: it })),
+    [props.disabledItems],
+  );
+
   return (
     <>
       <h3>Repositories *</h3>
@@ -41,6 +47,7 @@ export const BitbucketDataScope = ({ connectionId, onChangeItems, ...props }: Pr
       <DataScopeMillerColumns
         plugin="bitbucket"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/plugins/register/github/components/miller-columns/index.tsx b/config-ui/src/plugins/register/github/components/miller-columns/index.tsx
index 733793a2c..842de51c9 100644
--- a/config-ui/src/plugins/register/github/components/miller-columns/index.tsx
+++ b/config-ui/src/plugins/register/github/components/miller-columns/index.tsx
@@ -29,12 +29,14 @@ import { useMillerColumns } from './use-miller-columns';
 import * as S from './styled';
 
 interface Props extends UseMillerColumnsProps {
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const MillerColumns = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   const [selectedIds, setSelectedIds] = useState<McsID[]>([]);
+  const [disabledIds, setDisabledIds] = useState<McsID[]>([]);
 
   const { items, getHasMore, onExpand, onScroll } = useMillerColumns({
     connectionId,
@@ -44,6 +46,10 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
     setSelectedIds(selectedItems.map((it) => it.githubId));
   }, [selectedItems]);
 
+  useEffect(() => {
+    setDisabledIds((disabledItems ?? []).map((it) => it.githubId));
+  }, [disabledIds]);
+
   const handleChangeItems = (selectedIds: McsID[]) => {
     const result = selectedIds.map((id) => {
       const selectedItem = selectedItems.find((it) => it.githubId === id);
@@ -86,6 +92,7 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
       columnHeight={300}
       renderTitle={renderTitle}
       renderLoading={renderLoading}
+      disabledIds={disabledIds}
       selectedIds={selectedIds}
       onSelectItemIds={handleChangeItems}
     />
diff --git a/config-ui/src/plugins/register/github/components/repo-selector/index.tsx b/config-ui/src/plugins/register/github/components/repo-selector/index.tsx
index ce1d697b2..f4da815f4 100644
--- a/config-ui/src/plugins/register/github/components/repo-selector/index.tsx
+++ b/config-ui/src/plugins/register/github/components/repo-selector/index.tsx
@@ -16,8 +16,6 @@
  *
  */
 
-import React from 'react';
-
 import { MultiSelector } from '@/components';
 
 import { ScopeItemType } from '../../types';
@@ -25,11 +23,12 @@ import { ScopeItemType } from '../../types';
 import { useRepoSelector, UseRepoSelectorProps } from './use-repo-selector';
 
 interface Props extends UseRepoSelectorProps {
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const RepoSelector = ({ selectedItems, onChangeItems, ...props }: Props) => {
+export const RepoSelector = ({ disabledItems, selectedItems, onChangeItems, ...props }: Props) => {
   const { loading, items, onSearch } = useRepoSelector(props);
 
   return (
@@ -38,6 +37,7 @@ export const RepoSelector = ({ selectedItems, onChangeItems, ...props }: Props)
       items={items}
       getKey={(it) => it.githubId}
       getName={(it) => it.name}
+      disabledItems={disabledItems}
       selectedItems={selectedItems}
       onChangeItems={onChangeItems}
       loading={loading}
diff --git a/config-ui/src/plugins/register/github/data-scope.tsx b/config-ui/src/plugins/register/github/data-scope.tsx
index d63c71bad..7604c3cea 100644
--- a/config-ui/src/plugins/register/github/data-scope.tsx
+++ b/config-ui/src/plugins/register/github/data-scope.tsx
@@ -22,19 +22,30 @@ import * as S from './styled';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const GitHubDataScope = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const GitHubDataScope = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   return (
     <S.DataScope>
       <h3>Repositories *</h3>
       <p>Select the repositories you would like to sync.</p>
-      <MillerColumns connectionId={connectionId} selectedItems={selectedItems} onChangeItems={onChangeItems} />
+      <MillerColumns
+        connectionId={connectionId}
+        disabledItems={disabledItems}
+        selectedItems={selectedItems}
+        onChangeItems={onChangeItems}
+      />
       <h4>Add repositories outside of your organizations</h4>
       <p>Search for repositories and add to them</p>
-      <RepoSelector connectionId={connectionId} selectedItems={selectedItems} onChangeItems={onChangeItems} />
+      <RepoSelector
+        disabledItems={disabledItems}
+        connectionId={connectionId}
+        selectedItems={selectedItems}
+        onChangeItems={onChangeItems}
+      />
     </S.DataScope>
   );
 };
diff --git a/config-ui/src/plugins/register/gitlab/data-scope.tsx b/config-ui/src/plugins/register/gitlab/data-scope.tsx
index f3a8b63e5..13da84c89 100644
--- a/config-ui/src/plugins/register/gitlab/data-scope.tsx
+++ b/config-ui/src/plugins/register/gitlab/data-scope.tsx
@@ -25,6 +25,7 @@ import * as S from './styled';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
@@ -35,6 +36,11 @@ export const GitLabDataScope = ({ connectionId, onChangeItems, ...props }: Props
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: `${it.gitlabId}`, name: it.name, data: it })),
+    [props.disabledItems],
+  );
+
   return (
     <S.DataScope>
       <h3>Projects *</h3>
@@ -43,6 +49,7 @@ export const GitLabDataScope = ({ connectionId, onChangeItems, ...props }: Props
         title="Subgroups/Projects"
         plugin="gitlab"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
@@ -51,6 +58,7 @@ export const GitLabDataScope = ({ connectionId, onChangeItems, ...props }: Props
       <DataScopeSearch
         plugin="gitlab"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/plugins/register/jenkins/components/miller-columns/index.tsx b/config-ui/src/plugins/register/jenkins/components/miller-columns/index.tsx
index ca3827e05..9a934992e 100644
--- a/config-ui/src/plugins/register/jenkins/components/miller-columns/index.tsx
+++ b/config-ui/src/plugins/register/jenkins/components/miller-columns/index.tsx
@@ -28,12 +28,14 @@ import type { UseMillerColumnsProps, ExtraType } from './use-miller-columns';
 import { useMillerColumns } from './use-miller-columns';
 
 interface Props extends UseMillerColumnsProps {
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const MillerColumns = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   const [selectedIds, setSelectedIds] = useState<ID[]>([]);
+  const [disabledIds, setDisabledIds] = useState<ID[]>([]);
 
   const { items, getHasMore, onExpand } = useMillerColumns({
     connectionId,
@@ -43,6 +45,10 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
     setSelectedIds(selectedItems.map((it) => it.jobFullName));
   }, [selectedItems]);
 
+  useEffect(() => {
+    setDisabledIds((disabledItems ?? []).map((it) => it.jobFullName));
+  }, [disabledItems]);
+
   const handleChangeItems = (selectedIds: ID[]) => {
     const result = selectedIds.map((id) => {
       const selectedItem = selectedItems.find((it) => it.jobFullName === id);
@@ -75,6 +81,7 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
       columnHeight={300}
       getHasMore={getHasMore}
       renderLoading={renderLoading}
+      disabledIds={disabledIds}
       selectedIds={selectedIds}
       onSelectItemIds={handleChangeItems}
     />
diff --git a/config-ui/src/plugins/register/jenkins/data-scope.tsx b/config-ui/src/plugins/register/jenkins/data-scope.tsx
index 615447ebf..bf8322840 100644
--- a/config-ui/src/plugins/register/jenkins/data-scope.tsx
+++ b/config-ui/src/plugins/register/jenkins/data-scope.tsx
@@ -22,11 +22,12 @@ import { MillerColumns } from './components';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const JenkinsDataScope = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const JenkinsDataScope = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   return (
     <>
       <h3>Jobs *</h3>
diff --git a/config-ui/src/plugins/register/jira/components/miller-columns/index.tsx b/config-ui/src/plugins/register/jira/components/miller-columns/index.tsx
index 7bc1a7a70..d3bf209a2 100644
--- a/config-ui/src/plugins/register/jira/components/miller-columns/index.tsx
+++ b/config-ui/src/plugins/register/jira/components/miller-columns/index.tsx
@@ -28,12 +28,14 @@ import type { UseMillerColumnsProps } from './use-miller-columns';
 import { useMillerColumns } from './use-miller-columns';
 
 interface Props extends UseMillerColumnsProps {
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const MillerColumns = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   const [selectedIds, setSelectedIds] = useState<ID[]>([]);
+  const [disabledIds, setDisabledIds] = useState<ID[]>([]);
 
   const { items, getHasMore, onScroll } = useMillerColumns({
     connectionId,
@@ -43,6 +45,10 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
     setSelectedIds(selectedItems.map((it) => it.boardId));
   }, [selectedItems]);
 
+  useEffect(() => {
+    setDisabledIds((disabledItems ?? []).map((it) => it.boardId));
+  }, [disabledItems]);
+
   const handleChangeItems = (selectedIds: ID[]) => {
     const result = selectedIds.map((id) => {
       const selectedItem = selectedItems.find((it) => it.boardId === id);
@@ -76,6 +82,7 @@ export const MillerColumns = ({ connectionId, selectedItems, onChangeItems }: Pr
       columnCount={1}
       columnHeight={300}
       renderLoading={renderLoading}
+      disabledIds={disabledIds}
       selectedIds={selectedIds}
       onSelectItemIds={handleChangeItems}
     />
diff --git a/config-ui/src/plugins/register/jira/data-scope.tsx b/config-ui/src/plugins/register/jira/data-scope.tsx
index 0d5a45f07..15f4e3800 100644
--- a/config-ui/src/plugins/register/jira/data-scope.tsx
+++ b/config-ui/src/plugins/register/jira/data-scope.tsx
@@ -16,24 +16,28 @@
  *
  */
 
-import React from 'react';
-
 import type { ScopeItemType } from './types';
 
 import { MillerColumns } from './components/miller-columns';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
 
-export const JiraDataScope = ({ connectionId, selectedItems, onChangeItems }: Props) => {
+export const JiraDataScope = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => {
   return (
     <>
       <h3>Boards *</h3>
       <p>Select the boards you would like to sync.</p>
-      <MillerColumns connectionId={connectionId} selectedItems={selectedItems} onChangeItems={onChangeItems} />
+      <MillerColumns
+        connectionId={connectionId}
+        disabledItems={disabledItems}
+        selectedItems={selectedItems}
+        onChangeItems={onChangeItems}
+      />
     </>
   );
 };
diff --git a/config-ui/src/plugins/register/pagerduty/data-scope.tsx b/config-ui/src/plugins/register/pagerduty/data-scope.tsx
index 59c4e9d73..1bd6d2f11 100644
--- a/config-ui/src/plugins/register/pagerduty/data-scope.tsx
+++ b/config-ui/src/plugins/register/pagerduty/data-scope.tsx
@@ -25,6 +25,7 @@ import * as S from './styled';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
@@ -35,6 +36,11 @@ export const PagerDutyDataScope = ({ connectionId, onChangeItems, ...props }: Pr
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: `${it.id}`, name: it.name, data: it })),
+    [props.disabledItems],
+  );
+
   return (
     <S.DataScope>
       <h3>PagerDuty Services *</h3>
@@ -44,6 +50,7 @@ export const PagerDutyDataScope = ({ connectionId, onChangeItems, ...props }: Pr
         columnCount={1}
         plugin="pagerduty"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/plugins/register/sonarqube/data-scope.tsx b/config-ui/src/plugins/register/sonarqube/data-scope.tsx
index 3653f98ed..3d22360af 100644
--- a/config-ui/src/plugins/register/sonarqube/data-scope.tsx
+++ b/config-ui/src/plugins/register/sonarqube/data-scope.tsx
@@ -16,7 +16,7 @@
  *
  */
 
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
 
 import { DataScopeMillerColumns } from '@/plugins';
 
@@ -24,6 +24,7 @@ import type { SonarQubeScopeType } from './types';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: SonarQubeScopeType[];
   selectedItems: SonarQubeScopeType[];
   onChangeItems: (selectedItems: SonarQubeScopeType[]) => void;
 }
@@ -34,6 +35,11 @@ export const SonarQubeDataScope = ({ connectionId, onChangeItems, ...props }: Pr
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: it.projectKey, data: it })),
+    [props.disabledItems],
+  );
+
   return (
     <>
       <h4>Add Repositories by Selecting from the Directory</h4>
@@ -43,6 +49,7 @@ export const SonarQubeDataScope = ({ connectionId, onChangeItems, ...props }: Pr
         title="Projects"
         plugin="sonarqube"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/plugins/register/tapd/data-scope.tsx b/config-ui/src/plugins/register/tapd/data-scope.tsx
index 345cd73c2..4362b2f1d 100644
--- a/config-ui/src/plugins/register/tapd/data-scope.tsx
+++ b/config-ui/src/plugins/register/tapd/data-scope.tsx
@@ -16,7 +16,7 @@
  *
  */
 
-import React, { useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
 
 import { DataScopeMillerColumns } from '@/plugins';
 
@@ -27,6 +27,7 @@ import { ExternalLink } from '@/components';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
@@ -37,6 +38,11 @@ export const TapdDataScope = ({ connectionId, onChangeItems, ...props }: Props)
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () => (props.disabledItems ?? []).map((it) => ({ id: `${it.id}`, name: it.name, data: it })),
+    [props.disabledItems],
+  );
+
   const [pageToken, setPageToken] = useState<string | undefined>(undefined);
   const [companyId, setCompanyId] = useState<string>(
     localStorage.getItem(`plugin/tapd/connections/${connectionId}/company_id`) || '',
@@ -81,6 +87,7 @@ export const TapdDataScope = ({ connectionId, onChangeItems, ...props }: Props)
           key={pageToken}
           plugin="tapd"
           connectionId={connectionId}
+          disabledItems={disabledItems}
           selectedItems={selectedItems}
           onChangeItems={onChangeItems}
           pageToken={pageToken}
diff --git a/config-ui/src/plugins/register/zentao/data-scope.tsx b/config-ui/src/plugins/register/zentao/data-scope.tsx
index 601499135..ab2f60ea4 100644
--- a/config-ui/src/plugins/register/zentao/data-scope.tsx
+++ b/config-ui/src/plugins/register/zentao/data-scope.tsx
@@ -16,7 +16,7 @@
  *
  */
 
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
 
 import { DataScopeMillerColumns } from '@/plugins';
 
@@ -24,6 +24,7 @@ import type { ScopeItemType } from './types';
 
 interface Props {
   connectionId: ID;
+  disabledItems?: ScopeItemType[];
   selectedItems: ScopeItemType[];
   onChangeItems: (selectedItems: ScopeItemType[]) => void;
 }
@@ -39,6 +40,16 @@ export const ZentaoDataScope = ({ connectionId, onChangeItems, ...props }: Props
     [props.selectedItems],
   );
 
+  const disabledItems = useMemo(
+    () =>
+      (props.disabledItems ?? []).map((it) => ({
+        id: it.type === 'project' ? `project/${it.id}` : `product/${it.id}`,
+        name: it.name,
+        data: it,
+      })),
+    [props.disabledItems],
+  );
+
   return (
     <>
       <h3>Repositories *</h3>
@@ -46,6 +57,7 @@ export const ZentaoDataScope = ({ connectionId, onChangeItems, ...props }: Props
       <DataScopeMillerColumns
         plugin="zentao"
         connectionId={connectionId}
+        disabledItems={disabledItems}
         selectedItems={selectedItems}
         onChangeItems={onChangeItems}
       />
diff --git a/config-ui/src/store/index.ts b/config-ui/src/store/index.ts
index 4e6e8a4de..a247f9b21 100644
--- a/config-ui/src/store/index.ts
+++ b/config-ui/src/store/index.ts
@@ -17,3 +17,4 @@
  */
 
 export * from './connections';
+export * from './tips';
diff --git a/config-ui/src/plugins/register/jira/data-scope.tsx b/config-ui/src/store/tips/context.tsx
similarity index 57%
copy from config-ui/src/plugins/register/jira/data-scope.tsx
copy to config-ui/src/store/tips/context.tsx
index 0d5a45f07..51e9ffef1 100644
--- a/config-ui/src/plugins/register/jira/data-scope.tsx
+++ b/config-ui/src/store/tips/context.tsx
@@ -16,24 +16,31 @@
  *
  */
 
-import React from 'react';
+import React, { useState, useContext } from 'react';
 
-import type { ScopeItemType } from './types';
+const TipsContext = React.createContext<{
+  text: React.ReactNode;
+  setText: React.Dispatch<React.SetStateAction<React.ReactNode>>;
+}>({
+  text: '',
+  setText: () => {},
+});
 
-import { MillerColumns } from './components/miller-columns';
+export const TipsContextProvider = ({ children }: { children: React.ReactNode }) => {
+  const [text, setText] = useState<React.ReactNode>();
 
-interface Props {
-  connectionId: ID;
-  selectedItems: ScopeItemType[];
-  onChangeItems: (selectedItems: ScopeItemType[]) => void;
-}
-
-export const JiraDataScope = ({ connectionId, selectedItems, onChangeItems }: Props) => {
   return (
-    <>
-      <h3>Boards *</h3>
-      <p>Select the boards you would like to sync.</p>
-      <MillerColumns connectionId={connectionId} selectedItems={selectedItems} onChangeItems={onChangeItems} />
-    </>
+    <TipsContext.Provider
+      value={{
+        text,
+        setText,
+      }}
+    >
+      {children}
+    </TipsContext.Provider>
   );
 };
+
+export const TipsContextConsumer = TipsContext.Consumer;
+
+export const useTips = () => useContext(TipsContext);
diff --git a/config-ui/src/store/index.ts b/config-ui/src/store/tips/index.ts
similarity index 96%
copy from config-ui/src/store/index.ts
copy to config-ui/src/store/tips/index.ts
index 4e6e8a4de..5cdc9167c 100644
--- a/config-ui/src/store/index.ts
+++ b/config-ui/src/store/tips/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './connections';
+export * from './context';