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 8718e09e6 feat(config-ui): use new page connection home and detail 
(#5149)
8718e09e6 is described below

commit 8718e09e67bdd5238cfe181615f4260fe6bb85a5
Author: 青湛 <[email protected]>
AuthorDate: Thu May 11 09:43:06 2023 +0800

    feat(config-ui): use new page connection home and detail (#5149)
    
    * refactor(config-ui): adjust the style for dialog, table and page-header"
    
    * refactor(config-ui): the connection store
    
    * refactor(config-ui): the connection form plugin
    
    * fix(config-ui): format the misc in transformation form
    
    * refactor(config-ui): remove the connection submenu
    
    * feat(config-ui): use new page connection home and detail
---
 config-ui/src/App.tsx                              |   9 +-
 config-ui/src/components/dialog/styled.ts          |   4 +-
 config-ui/src/components/page-header/styled.ts     |   4 +-
 config-ui/src/components/table/styled.ts           |   8 +-
 config-ui/src/layouts/base/use-menu.ts             |   9 -
 .../pages/blueprint/create/components/step-1.tsx   |   4 +-
 .../src/pages/connection/{list => detail}/api.ts   |   0
 config-ui/src/pages/connection/detail/index.tsx    | 152 +++++++++++++++
 .../pages/connection/{list => detail}/styled.ts    |  39 ++--
 config-ui/src/pages/connection/form/index.tsx      |  44 -----
 config-ui/src/pages/connection/home/count.tsx      |  27 ---
 config-ui/src/pages/connection/home/index.tsx      | 212 +++++++++++++++------
 config-ui/src/pages/connection/home/styled.ts      |  10 +
 config-ui/src/pages/connection/index.ts            |   3 +-
 config-ui/src/pages/connection/list/connection.tsx | 142 --------------
 config-ui/src/pages/connection/list/index.tsx      |  46 -----
 .../plugins/components/connection-form/index.tsx   |  65 ++++++-
 .../components/connection-form/operate/index.ts    |  20 --
 .../components/connection-form/operate/save.tsx    |  65 -------
 .../components/connection-form/operate/test.tsx    |  65 -------
 .../plugins/components/connection-form/styled.ts   |   7 +-
 .../plugins/components/transformation-form/misc.ts |  12 +-
 config-ui/src/plugins/config.ts                    |   4 +-
 config-ui/src/plugins/register/ae/config.ts        |   2 +-
 config-ui/src/plugins/register/azure/config.tsx    |   7 +-
 .../src/plugins/register/bitbucket/config.tsx      |   4 +-
 config-ui/src/plugins/register/customize/config.ts |   2 +-
 config-ui/src/plugins/register/dbt/config.ts       |   2 +-
 config-ui/src/plugins/register/dora/config.ts      |   2 +-
 config-ui/src/plugins/register/feishu/config.ts    |   2 +-
 config-ui/src/plugins/register/gitee/config.ts     |   2 +-
 .../src/plugins/register/gitextractor/config.ts    |   2 +-
 config-ui/src/plugins/register/github/config.tsx   |   6 +-
 .../src/plugins/register/github_graphql/config.ts  |   2 +-
 config-ui/src/plugins/register/gitlab/config.tsx   |   6 +-
 config-ui/src/plugins/register/jenkins/config.ts   |   4 +-
 config-ui/src/plugins/register/jira/config.tsx     |   4 +-
 config-ui/src/plugins/register/org/config.ts       |   2 +-
 .../src/plugins/register/pagerduty/config.tsx      |   4 +-
 config-ui/src/plugins/register/refdiff/config.ts   |   2 +-
 config-ui/src/plugins/register/sonarqube/config.ts |   4 +-
 config-ui/src/plugins/register/starrocks/config.ts |   2 +-
 config-ui/src/plugins/register/tapd/config.tsx     |   7 +-
 .../src/plugins/register/teambition/config.tsx     |   6 +-
 config-ui/src/plugins/register/webook/config.ts    |   6 +-
 config-ui/src/plugins/register/zentao/config.ts    |   4 +-
 config-ui/src/store/connections/api.ts             |  13 +-
 config-ui/src/store/connections/context.tsx        |   4 +-
 config-ui/src/store/connections/status.tsx         |  47 ++---
 .../src/store/connections/use-context-value.ts     | 179 ++++++++---------
 50 files changed, 586 insertions(+), 692 deletions(-)

diff --git a/config-ui/src/App.tsx b/config-ui/src/App.tsx
index 1f24e9b27..51b14cb60 100644
--- a/config-ui/src/App.tsx
+++ b/config-ui/src/App.tsx
@@ -24,11 +24,10 @@ import { FromEnum } from '@/pages';
 import {
   OfflinePage,
   DBMigratePage,
+  ConnectionHomePage,
+  ConnectionDetailPage,
   ProjectHomePage,
   ProjectDetailPage,
-  ConnectionHomePage,
-  ConnectionListPage,
-  ConnectionFormPage,
   BlueprintHomePage,
   BlueprintCreatePage,
   BlueprintDetailPage,
@@ -69,9 +68,7 @@ function App() {
               <Switch>
                 <Route exact path="/" component={() => <Redirect 
to="/connections" />} />
                 <Route exact path="/connections" component={() => 
<ConnectionHomePage />} />
-                <Route exact path="/connections/:plugin" component={() => 
<ConnectionListPage />} />
-                <Route exact path="/connections/:plugin/create" component={() 
=> <ConnectionFormPage />} />
-                <Route exact path="/connections/:plugin/:cid" component={() => 
<ConnectionFormPage />} />
+                <Route exact path="/connections/:plugin/:id" component={() => 
<ConnectionDetailPage />} />
                 <Route exact path="/projects" component={() => 
<ProjectHomePage />} />
                 <Route exact path="/projects/:pname" component={() => 
<ProjectDetailPage />} />
                 <Route
diff --git a/config-ui/src/components/dialog/styled.ts 
b/config-ui/src/components/dialog/styled.ts
index 454fe0457..500401556 100644
--- a/config-ui/src/components/dialog/styled.ts
+++ b/config-ui/src/components/dialog/styled.ts
@@ -45,6 +45,8 @@ export const Header = styled.div`
   }
 `;
 
-export const Body = styled.div``;
+export const Body = styled.div`
+  margin: 24px;
+`;
 
 export const Footer = styled.div``;
diff --git a/config-ui/src/components/page-header/styled.ts 
b/config-ui/src/components/page-header/styled.ts
index 375bffec8..05ec967e5 100644
--- a/config-ui/src/components/page-header/styled.ts
+++ b/config-ui/src/components/page-header/styled.ts
@@ -24,7 +24,7 @@ export const Title = styled.div`
   display: flex;
   align-items: center;
   justify-content: space-between;
-  margin-bottom: 24px;
+  margin-bottom: 36px;
 `;
 
 export const Content = styled.div``;
@@ -35,7 +35,7 @@ export const Breadcrumbs = styled.ul`
 `;
 
 export const Breadcrumb = styled.li`
-  font-size: 20px;
+  font-size: 24px;
   font-weight: 600;
 
   a {
diff --git a/config-ui/src/components/table/styled.ts 
b/config-ui/src/components/table/styled.ts
index 59a76225c..c6ee203b6 100644
--- a/config-ui/src/components/table/styled.ts
+++ b/config-ui/src/components/table/styled.ts
@@ -32,13 +32,7 @@ export const THeader = styled.thead`
 
 export const TBody = styled.tbody``;
 
-export const TR = styled.tr`
-  &:last-child {
-    td {
-      border-bottom: none;
-    }
-  }
-`;
+export const TR = styled.tr``;
 
 export const TH = styled.th`
   padding: 12px 16px;
diff --git a/config-ui/src/layouts/base/use-menu.ts 
b/config-ui/src/layouts/base/use-menu.ts
index 864fae96d..9e87a25c4 100644
--- a/config-ui/src/layouts/base/use-menu.ts
+++ b/config-ui/src/layouts/base/use-menu.ts
@@ -19,8 +19,6 @@
 import { useMemo } from 'react';
 import { IconName } from '@blueprintjs/core';
 
-import { PluginConfig, PluginType } from '@/plugins';
-
 export type MenuItemType = {
   key: string;
   title: string;
@@ -42,13 +40,6 @@ export const useMenu = () => {
           title: 'Connections',
           icon: 'data-connection',
           path: '/connections',
-          children: PluginConfig.filter((p) => p.type === 
PluginType.Connection).map((it) => ({
-            key: it.plugin,
-            title: it.name,
-            iconUrl: it.icon,
-            path: `/connections/${it.plugin}`,
-            isBeta: it.isBeta,
-          })),
         },
         {
           key: 'project',
diff --git a/config-ui/src/pages/blueprint/create/components/step-1.tsx 
b/config-ui/src/pages/blueprint/create/components/step-1.tsx
index 725ac5918..1e7353296 100644
--- a/config-ui/src/pages/blueprint/create/components/step-1.tsx
+++ b/config-ui/src/pages/blueprint/create/components/step-1.tsx
@@ -92,7 +92,7 @@ export const Step1 = ({ from }: Props) => {
               onChangeItems={(selectedItems) => {
                 const lastItem = selectedItems[selectedItems.length - 1];
                 if (lastItem) {
-                  onTest(lastItem);
+                  onTest(lastItem.unique);
                 }
                 onChangeConnections(
                   selectedItems.map((sc) => {
@@ -124,7 +124,7 @@ export const Step1 = ({ from }: Props) => {
                           size={14}
                           icon="repeat"
                           style={{ marginRight: 4, cursor: 'pointer' }}
-                          onClick={() => onTest(cs)}
+                          onClick={() => onTest(cs.unique)}
                         />
                       )}
                       {cs.status}
diff --git a/config-ui/src/pages/connection/list/api.ts 
b/config-ui/src/pages/connection/detail/api.ts
similarity index 100%
rename from config-ui/src/pages/connection/list/api.ts
rename to config-ui/src/pages/connection/detail/api.ts
diff --git a/config-ui/src/pages/connection/detail/index.tsx 
b/config-ui/src/pages/connection/detail/index.tsx
new file mode 100644
index 000000000..4ce9a8dc3
--- /dev/null
+++ b/config-ui/src/pages/connection/detail/index.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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 { useState } from 'react';
+import { useParams, useHistory } from 'react-router-dom';
+import { Button, Icon, Intent } from '@blueprintjs/core';
+
+import { PageHeader, Dialog, IconButton } from '@/components';
+import { transformEntities } from '@/config';
+import { ConnectionForm } from '@/plugins';
+import type { ConnectionItemType } from '@/store';
+import { ConnectionContextProvider, useConnection, ConnectionStatus } from 
'@/store';
+import { operator } from '@/utils';
+
+import * as API from './api';
+import * as S from './styled';
+
+interface Props {
+  plugin: string;
+  id: ID;
+}
+
+const ConnectionDetail = ({ plugin, id }: Props) => {
+  const [type, setType] = useState<'deleteConnection' | 'updateConnection'>();
+  const [operating, setOperating] = useState(false);
+
+  const history = useHistory();
+  const { connections, onRefresh, onTest } = useConnection();
+  const { unique, status, name, icon, entities } = connections.find(
+    (cs) => cs.unique === `${plugin}-${id}`,
+  ) as ConnectionItemType;
+
+  const handleShowDeleteDialog = () => {
+    setType('deleteConnection');
+  };
+
+  const handleDelete = async () => {
+    const [success] = await operator(() => API.deleteConnection(plugin, id), {
+      setOperating,
+      formatMessage: () => 'Delete Connection Successful.',
+    });
+
+    if (success) {
+      history.push('/connections');
+    }
+  };
+
+  const handleShowUpdateDialog = () => {
+    setType('updateConnection');
+  };
+
+  const handleUpdate = () => {
+    setType(undefined);
+    onRefresh(plugin);
+  };
+
+  const handleHideDialog = () => {
+    setType(undefined);
+  };
+
+  return (
+    <PageHeader
+      breadcrumbs={[
+        { name: 'Connections', path: '/connections' },
+        { name, path: '' },
+      ]}
+      extra={<Button intent={Intent.DANGER} icon="trash" text="Delete 
Connection" onClick={handleShowDeleteDialog} />}
+    >
+      <S.Wrapper>
+        <div className="top">
+          <div className="entities">
+            <h3>Data Entities</h3>
+            <span>
+              {transformEntities(entities)
+                .map((it) => it.label)
+                .join(',')}
+            </span>
+          </div>
+          <div className="authentication">
+            <h3>
+              <span>Authentication</span>
+              <IconButton icon="annotation" tooltip="Edit Connection" 
onClick={handleShowUpdateDialog} />
+            </h3>
+            <span>Status: </span>
+            <span>
+              Status: <ConnectionStatus status={status} unique={unique} 
onTest={onTest} />
+            </span>
+          </div>
+        </div>
+      </S.Wrapper>
+      {type === 'deleteConnection' && (
+        <Dialog
+          isOpen
+          title="Would you like to delete this Data Connection?"
+          okText="Confirm"
+          okLoading={operating}
+          onCancel={handleHideDialog}
+          onOk={handleDelete}
+        >
+          <S.DialogBody>
+            <Icon icon="warning-sign" />
+            <span>
+              This operation cannot be undone. Deleting a Data Connection will 
delete all data that have been collected
+              in this Connection.
+            </span>
+          </S.DialogBody>
+        </Dialog>
+      )}
+      {type === 'updateConnection' && (
+        <Dialog
+          style={{ width: 820 }}
+          footer={null}
+          isOpen
+          title={
+            <S.DialogTitle>
+              <img src={icon} alt="" />
+              <span>Authentication</span>
+            </S.DialogTitle>
+          }
+          onCancel={handleHideDialog}
+        >
+          <ConnectionForm plugin={plugin} connectionId={id} 
onSuccess={handleUpdate} />
+        </Dialog>
+      )}
+    </PageHeader>
+  );
+};
+
+export const ConnectionDetailPage = () => {
+  const { plugin, id } = useParams<{ plugin: string; id: string }>();
+
+  return (
+    <ConnectionContextProvider plugin={plugin}>
+      <ConnectionDetail plugin={plugin} id={id} />
+    </ConnectionContextProvider>
+  );
+};
diff --git a/config-ui/src/pages/connection/list/styled.ts 
b/config-ui/src/pages/connection/detail/styled.ts
similarity index 70%
rename from config-ui/src/pages/connection/list/styled.ts
rename to config-ui/src/pages/connection/detail/styled.ts
index 9cf17b4e3..e8b6e0212 100644
--- a/config-ui/src/pages/connection/list/styled.ts
+++ b/config-ui/src/pages/connection/detail/styled.ts
@@ -19,29 +19,38 @@
 import styled from 'styled-components';
 
 export const Wrapper = styled.div`
-  .action {
-    margin-bottom: 16px;
+  .top {
+    display: flex;
+    justify-content: space-between;
+
+    h3 {
+      margin-bottom: 16px;
+    }
+  }
 
-    .bp4-button + .bp4-button {
-      margin-left: 8px;
+  .authentication {
+    h3 {
+      span 
     }
   }
 `;
 
-export const DeleteConfirm = styled.div`
-  padding: 16px 24px;
+export const DialogTitle = styled.div`
+  display: flex;
+  align-items: center;
 
-  h3 {
-    margin: 0;
-    padding: 0;
+  img {
+    margin-right: 8px;
+    width: 24px;
   }
+`;
 
-  p {
-    margin: 8px 0;
-  }
+export const DialogBody = styled.div`
+  display: flex;
+  align-items: center;
 
-  .bp4-button-group {
-    display: flex;
-    justify-content: flex-end;
+  .bp4-icon {
+    margin-right: 8px;
+    color: #f4be55;
   }
 `;
diff --git a/config-ui/src/pages/connection/form/index.tsx 
b/config-ui/src/pages/connection/form/index.tsx
deleted file mode 100644
index 518ea6a45..000000000
--- a/config-ui/src/pages/connection/form/index.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 { useParams } from 'react-router-dom';
-
-import { PageHeader } from '@/components';
-import { ConnectionForm, getPluginConfig } from '@/plugins';
-
-export const ConnectionFormPage = () => {
-  const { plugin, cid } = useParams<{ plugin: string; cid?: string }>();
-
-  const { name } = useMemo(() => getPluginConfig(plugin), [plugin]);
-
-  return (
-    <PageHeader
-      breadcrumbs={[
-        { name: 'Connections', path: '/connections' },
-        { name, path: `/connections/${plugin}` },
-        {
-          name: cid ? cid : 'Create a New Connection',
-          path: `/connections/${plugin}/${cid ? cid : 'create'}`,
-        },
-      ]}
-    >
-      <ConnectionForm plugin={plugin} connectionId={cid} />
-    </PageHeader>
-  );
-};
diff --git a/config-ui/src/pages/connection/home/count.tsx 
b/config-ui/src/pages/connection/home/count.tsx
deleted file mode 100644
index 78fce8afe..000000000
--- a/config-ui/src/pages/connection/home/count.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 * as S from './styled';
-
-interface Props {
-  count: number;
-}
-
-export const Count = ({ count }: Props) => {
-  return <S.Count>{count ? `${count} connections` : 'No connection'}</S.Count>;
-};
diff --git a/config-ui/src/pages/connection/home/index.tsx 
b/config-ui/src/pages/connection/home/index.tsx
index af62514d9..4367d0143 100644
--- a/config-ui/src/pages/connection/home/index.tsx
+++ b/config-ui/src/pages/connection/home/index.tsx
@@ -16,77 +16,175 @@
  *
  */
 
-import { useMemo } from 'react';
-import { useHistory } from 'react-router-dom';
-import { Tag, Intent } from '@blueprintjs/core';
+import { useState, useMemo } from 'react';
+import { Link } from 'react-router-dom';
+import { Tag, Intent, Button } from '@blueprintjs/core';
 
+import { Dialog, Table } from '@/components';
 import type { PluginConfigType } from '@/plugins';
-import { PluginConfig, PluginType } from '@/plugins';
-import { ConnectionContextProvider, ConnectionContextConsumer } from '@/store';
+import { PluginConfig, PluginType, ConnectionForm } from '@/plugins';
+import { ConnectionContextProvider, useConnection, ConnectionStatus } from 
'@/store';
 
-import { Count } from './count';
 import * as S from './styled';
 
-export const ConnectionHomePage = () => {
-  const history = useHistory();
+export const ConnectionHome = () => {
+  const [type, setType] = useState<'list' | 'form'>();
+  const [pluginConfig, setPluginConfig] = useState<PluginConfigType>();
+
+  const { connections, onRefresh, onTest } = useConnection();
 
   const [plugins, webhook] = useMemo(
     () => [
-      PluginConfig.filter((p) => p.type === PluginType.Connection && p.plugin 
!== 'webhook'),
-      PluginConfig.find((p) => p.plugin === 'webhook') as PluginConfigType,
+      PluginConfig.filter((p) => p.type === PluginType.Connection && p.plugin 
!== 'webhook').map((p) => ({
+        ...p,
+        count: connections.filter((cs) => cs.plugin === p.plugin).length,
+      })),
+      {
+        ...(PluginConfig.find((p) => p.plugin === 'webhook') as 
PluginConfigType),
+        count: connections.filter((cs) => cs.plugin === 'webhook').length,
+      },
     ],
     [],
   );
 
+  const handleShowListDialog = (config: PluginConfigType) => {
+    setType('list');
+    setPluginConfig(config);
+  };
+
+  const handleShowFormDialog = () => {
+    setType('form');
+  };
+
+  const handleHideDialog = () => {
+    setType(undefined);
+    setPluginConfig(undefined);
+  };
+
+  const handleCreateSuccess = async (unqie: string, plugin: string) => {
+    onRefresh(plugin);
+    setType('list');
+  };
+
+  return (
+    <S.Wrapper>
+      <div className="block">
+        <h1>Connections</h1>
+        <h5>
+          Create and manage data connections from the following data sources 
or Webhooks to be used in syncing data in
+          your Projects.
+        </h5>
+      </div>
+      <div className="block">
+        <h2>Data Connections</h2>
+        <h5>
+          You can create and manage data connections for the following data 
sources and use them in your Projects.
+        </h5>
+        <ul>
+          {plugins.map((p) => (
+            <li key={p.plugin} onClick={() => handleShowListDialog(p)}>
+              <img src={p.icon} alt="" />
+              <span className="name">{p.name}</span>
+              <S.Count>{p.count ? `${p.count} connections` : 'No 
connection'}</S.Count>
+              {p.isBeta && (
+                <Tag intent={Intent.WARNING} round>
+                  beta
+                </Tag>
+              )}
+            </li>
+          ))}
+        </ul>
+      </div>
+      <div className="block">
+        <h2>Webhooks</h2>
+        <h5>
+          You can use webhooks to import deployments and incidents from the 
unsupported data integrations to calculate
+          DORA metrics, etc.
+        </h5>
+        <ul>
+          <li onClick={() => handleShowListDialog(webhook)}>
+            <img src={webhook.icon} alt="" />
+            <span className="name">{webhook.name}</span>
+            <S.Count>{webhook.count ? `${webhook.count} connections` : 'No 
connection'}</S.Count>
+          </li>
+        </ul>
+      </div>
+      {type === 'list' && pluginConfig && (
+        <Dialog
+          style={{ width: 820 }}
+          isOpen
+          title={
+            <S.DialogTitle>
+              <img src={pluginConfig.icon} alt="" />
+              <span>Manage Connections: {pluginConfig.name}</span>
+            </S.DialogTitle>
+          }
+          footer={null}
+          onCancel={handleHideDialog}
+        >
+          <Table
+            noShadow
+            columns={[
+              {
+                title: 'Connection Name',
+                dataIndex: 'name',
+                key: 'name',
+              },
+              {
+                title: 'Status',
+                dataIndex: ['status', 'unique'],
+                key: 'status',
+                render: ({ status, unique }) => <ConnectionStatus 
status={status} unique={unique} onTest={onTest} />,
+              },
+              {
+                title: '',
+                dataIndex: ['plugin', 'id'],
+                key: 'link',
+                width: 100,
+                render: ({ plugin, id }) => <Link 
to={`/connections/${plugin}/${id}`}>Details</Link>,
+              },
+            ]}
+            dataSource={connections.filter((cs) => cs.plugin === 
pluginConfig.plugin)}
+            noData={{
+              text: 'There is no data connection yet. Please add a new 
connection.',
+            }}
+          />
+          <Button
+            style={{ marginTop: 16 }}
+            intent={Intent.PRIMARY}
+            icon="add"
+            text="Create a New Connection"
+            onClick={handleShowFormDialog}
+          />
+        </Dialog>
+      )}
+      {type === 'form' && pluginConfig && (
+        <Dialog
+          style={{ width: 820 }}
+          isOpen
+          title={
+            <S.DialogTitle>
+              <img src={pluginConfig.icon} alt="" />
+              <span>Manage Connections: {pluginConfig.name}</span>
+            </S.DialogTitle>
+          }
+          footer={null}
+          onCancel={handleHideDialog}
+        >
+          <ConnectionForm
+            plugin={pluginConfig.plugin}
+            onSuccess={(unique) => handleCreateSuccess(unique, 
pluginConfig.plugin)}
+          />
+        </Dialog>
+      )}
+    </S.Wrapper>
+  );
+};
+
+export const ConnectionHomePage = () => {
   return (
     <ConnectionContextProvider>
-      <ConnectionContextConsumer>
-        {({ connections }) => (
-          <S.Wrapper>
-            <div className="block">
-              <h1>Connections</h1>
-              <h5>
-                Create and manage data connections from the following data 
sources or Webhooks to be used in syncing
-                data in your Projects.
-              </h5>
-            </div>
-            <div className="block">
-              <h2>Data Connections</h2>
-              <h5>
-                You can create and manage data connections for the following 
data sources and use them in your Projects.
-              </h5>
-              <ul>
-                {plugins.map((p) => (
-                  <li key={p.plugin} onClick={() => 
history.push(`/connections/${p.plugin}`)}>
-                    <img src={p.icon} alt="" />
-                    <span className="name">{p.name}</span>
-                    <Count count={connections.filter((cs) => cs.plugin === 
p.plugin).length} />
-                    {p.isBeta && (
-                      <Tag intent={Intent.WARNING} round>
-                        beta
-                      </Tag>
-                    )}
-                  </li>
-                ))}
-              </ul>
-            </div>
-            <div className="block">
-              <h2>Webhooks</h2>
-              <h5>
-                You can use webhooks to import deployments and incidents from 
the unsupported data integrations to
-                calculate DORA metrics, etc.
-              </h5>
-              <ul>
-                <li onClick={() => 
history.push(`/connections/${webhook.plugin}`)}>
-                  <img src={webhook.icon} alt="" />
-                  <span className="name">{webhook.name}</span>
-                  <Count count={connections.filter((cs) => cs.plugin === 
'webhook').length} />
-                </li>
-              </ul>
-            </div>
-          </S.Wrapper>
-        )}
-      </ConnectionContextConsumer>
+      <ConnectionHome />
     </ConnectionContextProvider>
   );
 };
diff --git a/config-ui/src/pages/connection/home/styled.ts 
b/config-ui/src/pages/connection/home/styled.ts
index 05cba9ee9..601ae9f42 100644
--- a/config-ui/src/pages/connection/home/styled.ts
+++ b/config-ui/src/pages/connection/home/styled.ts
@@ -85,3 +85,13 @@ export const Wrapper = styled.div`
 export const Count = styled.span`
   color: #70727f;
 `;
+
+export const DialogTitle = styled.div`
+  display: flex;
+  align-items: center;
+
+  img {
+    margin-right: 8px;
+    width: 24px;
+  }
+`;
diff --git a/config-ui/src/pages/connection/index.ts 
b/config-ui/src/pages/connection/index.ts
index cdf832ea7..d5131a681 100644
--- a/config-ui/src/pages/connection/index.ts
+++ b/config-ui/src/pages/connection/index.ts
@@ -17,5 +17,4 @@
  */
 
 export * from './home';
-export * from './list';
-export * from './form';
+export * from './detail';
diff --git a/config-ui/src/pages/connection/list/connection.tsx 
b/config-ui/src/pages/connection/list/connection.tsx
deleted file mode 100644
index 5ef877039..000000000
--- a/config-ui/src/pages/connection/list/connection.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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, useMemo } from 'react';
-import { useHistory } from 'react-router-dom';
-import { ButtonGroup, Button, Intent, Position } from '@blueprintjs/core';
-import { Popover2 } from '@blueprintjs/popover2';
-
-import { Table, ColumnType, IconButton } from '@/components';
-import type { ConnectionItemType } from '@/store';
-import { useConnection, ConnectionStatus } from '@/store';
-import { operator } from '@/utils';
-
-import * as API from './api';
-import * as S from './styled';
-
-interface Props {
-  plugin: string;
-}
-
-export const Connection = ({ plugin }: Props) => {
-  const [operating, setOperating] = useState(false);
-
-  const history = useHistory();
-
-  const { connections, onTest, onRefresh } = useConnection();
-
-  useEffect(() => {
-    connections.map((cs) => onTest(cs));
-  }, []);
-
-  const handleRefresh = () => onRefresh();
-
-  const handleCreate = () => history.push(`/connections/${plugin}/create`);
-
-  const handleUpdate = (id: ID) => 
history.push(`/connections/${plugin}/${id}`);
-
-  const handleDelete = async (id: ID) => {
-    const [success] = await operator(() => API.deleteConnection(plugin, id), {
-      setOperating,
-    });
-
-    if (success) {
-      onRefresh();
-    }
-  };
-
-  const columns = useMemo(
-    () =>
-      [
-        {
-          title: 'ID',
-          dataIndex: 'id',
-          key: 'id',
-          width: 100,
-        },
-        {
-          title: 'Connection Name',
-          dataIndex: 'name',
-          key: 'name',
-        },
-        {
-          title: 'Endpoint',
-          dataIndex: 'endpoint',
-          key: 'endpoint',
-          ellipsis: true,
-        },
-        {
-          title: 'Status',
-          dataIndex: 'status',
-          key: 'status',
-          align: 'center',
-          render: (_, row) => <ConnectionStatus connection={row} 
onTest={onTest} />,
-        },
-        {
-          title: '',
-          dataIndex: 'id',
-          key: 'action',
-          width: 100,
-          align: 'center',
-          render: (id) => (
-            <ButtonGroup>
-              <IconButton icon="edit" tooltip="Edit" onClick={() => 
handleUpdate(id)} />
-              <Popover2
-                position={Position.TOP}
-                content={
-                  <S.DeleteConfirm>
-                    <h3>Confirm deletion</h3>
-                    <p>Are you sure you want to delete this item?</p>
-                    <ButtonGroup>
-                      <Button
-                        loading={operating}
-                        intent={Intent.DANGER}
-                        text="Delete"
-                        onClick={() => handleDelete(id)}
-                      />
-                    </ButtonGroup>
-                  </S.DeleteConfirm>
-                }
-              >
-                <IconButton icon="delete" tooltip="Delete" />
-              </Popover2>
-            </ButtonGroup>
-          ),
-        },
-      ] as ColumnType<ConnectionItemType>,
-    [],
-  );
-
-  return (
-    <S.Wrapper>
-      <ButtonGroup className="action">
-        <Button intent={Intent.PRIMARY} icon="plus" text="New Connection" 
onClick={handleCreate} />
-        <Button icon="refresh" text="Refresh Connections" 
onClick={handleRefresh} />
-      </ButtonGroup>
-      <Table
-        columns={columns}
-        dataSource={connections}
-        noData={{
-          text: 'There is no data connection yet. Please add a new 
connection.',
-          btnText: 'New Connection',
-          onCreate: handleCreate,
-        }}
-      />
-    </S.Wrapper>
-  );
-};
diff --git a/config-ui/src/pages/connection/list/index.tsx 
b/config-ui/src/pages/connection/list/index.tsx
deleted file mode 100644
index e44bd603a..000000000
--- a/config-ui/src/pages/connection/list/index.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 { useParams } from 'react-router-dom';
-
-import { PageHeader } from '@/components';
-import { getPluginConfig } from '@/plugins';
-import { WebHookConnection } from '@/plugins/register/webook';
-import { ConnectionContextProvider } from '@/store';
-
-import { Connection } from './connection';
-
-export const ConnectionListPage = () => {
-  const { plugin } = useParams<{ plugin: string }>();
-
-  const config = useMemo(() => getPluginConfig(plugin), [plugin]);
-
-  return (
-    <ConnectionContextProvider plugin={plugin}>
-      <PageHeader
-        breadcrumbs={[
-          { name: 'Connections', path: '/connections' },
-          { name: config.name, path: `/connections/${plugin}` },
-        ]}
-      >
-        {plugin === 'webhook' ? <WebHookConnection /> : <Connection 
plugin={plugin} />}
-      </PageHeader>
-    </ConnectionContextProvider>
-  );
-};
diff --git a/config-ui/src/plugins/components/connection-form/index.tsx 
b/config-ui/src/plugins/components/connection-form/index.tsx
index f9ea7c88c..b2ec47e62 100644
--- a/config-ui/src/plugins/components/connection-form/index.tsx
+++ b/config-ui/src/plugins/components/connection-form/index.tsx
@@ -16,32 +16,39 @@
  *
  */
 
-import React, { useMemo, useState } from 'react';
-import { ButtonGroup } from '@blueprintjs/core';
+import { useMemo, useState } from 'react';
+import { ButtonGroup, Button, Intent } from '@blueprintjs/core';
+import { pick } from 'lodash';
 
 import { ExternalLink, PageLoading } from '@/components';
 import { useRefreshData } from '@/hooks';
 import { getPluginConfig } from '@/plugins';
+import { operator } from '@/utils';
 
 import { Form } from './fields';
-import { Save, Test } from './operate';
 import * as API from './api';
 import * as S from './styled';
 
 interface Props {
   plugin: string;
   connectionId?: ID;
+  onSuccess?: (unique: string) => void;
 }
 
-export const ConnectionForm = ({ plugin, connectionId }: Props) => {
+export const ConnectionForm = ({ plugin, connectionId, onSuccess }: Props) => {
   const [values, setValues] = useState<Record<string, any>>({});
   const [errors, setErrors] = useState<Record<string, any>>({});
+  const [operating, setOperating] = useState(false);
 
   const {
     name,
     connection: { docLink, fields, initialValues },
   } = useMemo(() => getPluginConfig(plugin), [plugin]);
 
+  const disabled = useMemo(() => {
+    return Object.values(errors).some((value) => value);
+  }, [errors]);
+
   const { ready, data } = useRefreshData(async () => {
     if (!connectionId) {
       return {};
@@ -50,6 +57,45 @@ export const ConnectionForm = ({ plugin, connectionId }: 
Props) => {
     return API.getConnection(plugin, connectionId);
   }, [plugin, connectionId]);
 
+  const handleTest = async () => {
+    await operator(
+      () =>
+        API.testConnection(
+          plugin,
+          pick(values, [
+            'endpoint',
+            'token',
+            'username',
+            'password',
+            'proxy',
+            'authMethod',
+            'appId',
+            'secretKey',
+            'tenantId',
+            'tenantType',
+          ]),
+        ),
+      {
+        setOperating,
+        formatMessage: () => 'Test Connection Successfully.',
+      },
+    );
+  };
+
+  const handleSave = async () => {
+    const [success, res] = await operator(
+      () => (!connectionId ? API.createConnection(plugin, values) : 
API.updateConnection(plugin, connectionId, values)),
+      {
+        setOperating,
+        formatMessage: () => (!connectionId ? 'Create a New Connection 
Successful.' : 'Update Connection Successful.'),
+      },
+    );
+
+    if (success) {
+      onSuccess?.(`${plugin}-${res.id}`);
+    }
+  };
+
   if (connectionId && !ready) {
     return <PageLoading />;
   }
@@ -71,8 +117,15 @@ export const ConnectionForm = ({ plugin, connectionId }: 
Props) => {
           setErrors={setErrors}
         />
         <ButtonGroup className="btns">
-          <Test plugin={plugin} values={values} errors={errors} />
-          <Save plugin={plugin} connectionId={connectionId} values={values} 
errors={errors} />
+          <Button loading={operating} disabled={disabled} outlined text="Test 
Connection" onClick={handleTest} />
+          <Button
+            loading={operating}
+            disabled={disabled}
+            intent={Intent.PRIMARY}
+            outlined
+            text="Save Connection"
+            onClick={handleSave}
+          />
         </ButtonGroup>
       </S.Form>
     </S.Wrapper>
diff --git a/config-ui/src/plugins/components/connection-form/operate/index.ts 
b/config-ui/src/plugins/components/connection-form/operate/index.ts
deleted file mode 100644
index 2dda77f3f..000000000
--- a/config-ui/src/plugins/components/connection-form/operate/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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 './test';
-export * from './save';
diff --git a/config-ui/src/plugins/components/connection-form/operate/save.tsx 
b/config-ui/src/plugins/components/connection-form/operate/save.tsx
deleted file mode 100644
index a4143b806..000000000
--- a/config-ui/src/plugins/components/connection-form/operate/save.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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 { useState, useMemo } from 'react';
-import { useHistory } from 'react-router-dom';
-import { Button, Intent } from '@blueprintjs/core';
-
-import { operator } from '@/utils';
-
-import * as API from '../api';
-
-interface Props {
-  plugin: string;
-  connectionId?: ID;
-  values: any;
-  errors: any;
-}
-
-export const Save = ({ plugin, connectionId, values, errors }: Props) => {
-  const [saving, setSaving] = useState(false);
-  const history = useHistory();
-
-  const handleSubmit = async () => {
-    const [success] = await operator(
-      () => (!connectionId ? API.createConnection(plugin, values) : 
API.updateConnection(plugin, connectionId, values)),
-      {
-        setOperating: setSaving,
-      },
-    );
-
-    if (success) {
-      history.push(`/connections/${plugin}`);
-    }
-  };
-
-  const disabled = useMemo(() => {
-    return Object.values(errors).some((value) => value);
-  }, [errors]);
-
-  return (
-    <Button
-      loading={saving}
-      disabled={disabled}
-      intent={Intent.PRIMARY}
-      outlined
-      text="Save Connection"
-      onClick={handleSubmit}
-    />
-  );
-};
diff --git a/config-ui/src/plugins/components/connection-form/operate/test.tsx 
b/config-ui/src/plugins/components/connection-form/operate/test.tsx
deleted file mode 100644
index f2222a691..000000000
--- a/config-ui/src/plugins/components/connection-form/operate/test.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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 { useState, useMemo } from 'react';
-import { Button } from '@blueprintjs/core';
-import { pick } from 'lodash';
-
-import { operator } from '@/utils';
-
-import * as API from '../api';
-
-interface Props {
-  plugin: string;
-  values: any;
-  errors: any;
-}
-
-export const Test = ({ plugin, values, errors }: Props) => {
-  const [testing, setTesting] = useState(false);
-
-  const disabled = useMemo(() => {
-    return Object.values(errors).some((value) => value);
-  }, [errors]);
-
-  const handleSubmit = async () => {
-    await operator(
-      () =>
-        API.testConnection(
-          plugin,
-          pick(values, [
-            'endpoint',
-            'token',
-            'username',
-            'password',
-            'proxy',
-            'authMethod',
-            'appId',
-            'secretKey',
-            'tenantId',
-            'tenantType',
-          ]),
-        ),
-      {
-        setOperating: setTesting,
-      },
-    );
-  };
-
-  return <Button loading={testing} disabled={disabled} outlined text="Test 
Connection" onClick={handleSubmit} />;
-};
diff --git a/config-ui/src/plugins/components/connection-form/styled.ts 
b/config-ui/src/plugins/components/connection-form/styled.ts
index 09c870e4d..8012bf159 100644
--- a/config-ui/src/plugins/components/connection-form/styled.ts
+++ b/config-ui/src/plugins/components/connection-form/styled.ts
@@ -21,7 +21,7 @@ import styled from 'styled-components';
 export const Wrapper = styled.div``;
 
 export const Tips = styled.div`
-  margin-bottom: 36px;
+  margin-bottom: 24px;
   padding: 24px;
   color: #3c5088;
   background: #f0f4fe;
@@ -30,11 +30,6 @@ export const Tips = styled.div`
 `;
 
 export const Form = styled.div`
-  padding: 24px;
-  background: #ffffff;
-  box-shadow: 0px 2.4px 4.8px -0.8px rgba(0, 0, 0, 0.1), 0px 1.6px 8px rgba(0, 
0, 0, 0.07);
-  border-radius: 8px;
-
   .bp4-form-group label.bp4-label {
     margin: 0 0 8px 0;
   }
diff --git a/config-ui/src/plugins/components/transformation-form/misc.ts 
b/config-ui/src/plugins/components/transformation-form/misc.ts
index b330ba7cf..8cb8bc2c7 100644
--- a/config-ui/src/plugins/components/transformation-form/misc.ts
+++ b/config-ui/src/plugins/components/transformation-form/misc.ts
@@ -23,7 +23,7 @@ export const TIPS_MAP: Record<string, { name: string; link: 
string }> = {
   },
   gitlab: {
     name: 'GitLab',
-    link: 
'https://devlake.apache.org/docs/Configuration/GitLab#step-3---adding-transformation-rules-optional'
+    link: 
'https://devlake.apache.org/docs/Configuration/GitLab#step-3---adding-transformation-rules-optional',
   },
   jira: {
     name: 'Jira',
@@ -31,18 +31,18 @@ export const TIPS_MAP: Record<string, { name: string; link: 
string }> = {
   },
   jenkins: {
     name: 'Jenkins',
-    link: 
'https://devlake.apache.org/docs/Configuration/Jenkins#step-3---adding-transformation-rules-optional'
+    link: 
'https://devlake.apache.org/docs/Configuration/Jenkins#step-3---adding-transformation-rules-optional',
   },
   bitbucket: {
     name: 'BitBucket',
-    link: 
'https://devlake.apache.org/docs/Configuration/BitBucket#step-3---adding-transformation-rules-optional'
+    link: 
'https://devlake.apache.org/docs/Configuration/BitBucket#step-3---adding-transformation-rules-optional',
   },
   azuredevops: {
     name: 'Azure DevOps',
-    link: 
'https://devlake.apache.org/docs/Configuration/Jenkins#step-3---adding-transformation-rules-optional'
+    link: 
'https://devlake.apache.org/docs/Configuration/Jenkins#step-3---adding-transformation-rules-optional',
   },
   tapd: {
-    name:'TAPD',
-    link: 
'https://devlake.apache.org/docs/Configuration/Tapd#step-3---adding-transformation-rules-optional'
+    name: 'TAPD',
+    link: 
'https://devlake.apache.org/docs/Configuration/Tapd#step-3---adding-transformation-rules-optional',
   },
 };
diff --git a/config-ui/src/plugins/config.ts b/config-ui/src/plugins/config.ts
index 542e6c7a8..29fefa6c4 100644
--- a/config-ui/src/plugins/config.ts
+++ b/config-ui/src/plugins/config.ts
@@ -17,6 +17,7 @@
  */
 
 import type { PluginConfigType } from './types';
+import { BasePipelineConfig } from './register/base';
 import { AEConfig } from './register/ae';
 import { AzureConfig } from './register/azure';
 import { BitBucketConfig } from './register/bitbucket';
@@ -38,9 +39,8 @@ import { SonarQubeConfig } from './register/sonarqube';
 import { StarRocksConfig } from './register/starrocks';
 import { TAPDConfig } from './register/tapd';
 import { WebhookConfig } from './register/webook';
-import { ZenTaoConfig } from './register/zentao';
 import { TeambitionConfig } from './register/teambition';
-import { BasePipelineConfig } from '@/plugins/register/base';
+import { ZenTaoConfig } from './register/zentao';
 
 export const PluginConfig: PluginConfigType[] = [
   AEConfig,
diff --git a/config-ui/src/plugins/register/ae/config.ts 
b/config-ui/src/plugins/register/ae/config.ts
index f0d873cb3..efcf1b87b 100644
--- a/config-ui/src/plugins/register/ae/config.ts
+++ b/config-ui/src/plugins/register/ae/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/azure/config.tsx 
b/config-ui/src/plugins/register/azure/config.tsx
index eea381cf2..0686b3a9c 100644
--- a/config-ui/src/plugins/register/azure/config.tsx
+++ b/config-ui/src/plugins/register/azure/config.tsx
@@ -16,11 +16,10 @@
  *
  */
 
-import React from 'react';
-
 import { ExternalLink } from '@/components';
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { BaseURL } from './connection-fields';
diff --git a/config-ui/src/plugins/register/bitbucket/config.tsx 
b/config-ui/src/plugins/register/bitbucket/config.tsx
index 8d7edb9ea..7518bd041 100644
--- a/config-ui/src/plugins/register/bitbucket/config.tsx
+++ b/config-ui/src/plugins/register/bitbucket/config.tsx
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/plugins/register/customize/config.ts 
b/config-ui/src/plugins/register/customize/config.ts
index e5bebbb5d..6def51839 100644
--- a/config-ui/src/plugins/register/customize/config.ts
+++ b/config-ui/src/plugins/register/customize/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/dbt/config.ts 
b/config-ui/src/plugins/register/dbt/config.ts
index 8b19b8ea9..90b1d3e63 100644
--- a/config-ui/src/plugins/register/dbt/config.ts
+++ b/config-ui/src/plugins/register/dbt/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/dora/config.ts 
b/config-ui/src/plugins/register/dora/config.ts
index fe7f06084..5589dc87b 100644
--- a/config-ui/src/plugins/register/dora/config.ts
+++ b/config-ui/src/plugins/register/dora/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/feishu/config.ts 
b/config-ui/src/plugins/register/feishu/config.ts
index 6be499ffb..6c1a347f7 100644
--- a/config-ui/src/plugins/register/feishu/config.ts
+++ b/config-ui/src/plugins/register/feishu/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/gitee/config.ts 
b/config-ui/src/plugins/register/gitee/config.ts
index 64d528f49..39267405a 100644
--- a/config-ui/src/plugins/register/gitee/config.ts
+++ b/config-ui/src/plugins/register/gitee/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/gitextractor/config.ts 
b/config-ui/src/plugins/register/gitextractor/config.ts
index 400941ead..20d746fd9 100644
--- a/config-ui/src/plugins/register/gitextractor/config.ts
+++ b/config-ui/src/plugins/register/gitextractor/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/github/config.tsx 
b/config-ui/src/plugins/register/github/config.tsx
index a5976b0c4..85698f4bd 100644
--- a/config-ui/src/plugins/register/github/config.tsx
+++ b/config-ui/src/plugins/register/github/config.tsx
@@ -16,10 +16,8 @@
  *
  */
 
-import React from 'react';
-
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { Token, Graphql } from './connection-fields';
diff --git a/config-ui/src/plugins/register/github_graphql/config.ts 
b/config-ui/src/plugins/register/github_graphql/config.ts
index c21edbfba..ca1c8e911 100644
--- a/config-ui/src/plugins/register/github_graphql/config.ts
+++ b/config-ui/src/plugins/register/github_graphql/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/gitlab/config.tsx 
b/config-ui/src/plugins/register/gitlab/config.tsx
index bf24eefea..19ebbe925 100644
--- a/config-ui/src/plugins/register/gitlab/config.tsx
+++ b/config-ui/src/plugins/register/gitlab/config.tsx
@@ -16,10 +16,8 @@
  *
  */
 
-import React from 'react';
-
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import { ExternalLink } from '@/components';
 
diff --git a/config-ui/src/plugins/register/jenkins/config.ts 
b/config-ui/src/plugins/register/jenkins/config.ts
index 354159392..ac63788a0 100644
--- a/config-ui/src/plugins/register/jenkins/config.ts
+++ b/config-ui/src/plugins/register/jenkins/config.ts
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/plugins/register/jira/config.tsx 
b/config-ui/src/plugins/register/jira/config.tsx
index 4aebf5158..30915cc3a 100644
--- a/config-ui/src/plugins/register/jira/config.tsx
+++ b/config-ui/src/plugins/register/jira/config.tsx
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { Auth } from './connection-fields';
diff --git a/config-ui/src/plugins/register/org/config.ts 
b/config-ui/src/plugins/register/org/config.ts
index 5b47e7af6..d39717a5b 100644
--- a/config-ui/src/plugins/register/org/config.ts
+++ b/config-ui/src/plugins/register/org/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/pagerduty/config.tsx 
b/config-ui/src/plugins/register/pagerduty/config.tsx
index b7d2aa1d1..56d1f8a0a 100644
--- a/config-ui/src/plugins/register/pagerduty/config.tsx
+++ b/config-ui/src/plugins/register/pagerduty/config.tsx
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/plugins/register/refdiff/config.ts 
b/config-ui/src/plugins/register/refdiff/config.ts
index 916696dfc..ff7864b87 100644
--- a/config-ui/src/plugins/register/refdiff/config.ts
+++ b/config-ui/src/plugins/register/refdiff/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/sonarqube/config.ts 
b/config-ui/src/plugins/register/sonarqube/config.ts
index 37dfd2c33..f8dce2c3f 100644
--- a/config-ui/src/plugins/register/sonarqube/config.ts
+++ b/config-ui/src/plugins/register/sonarqube/config.ts
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/plugins/register/starrocks/config.ts 
b/config-ui/src/plugins/register/starrocks/config.ts
index 1a24fac9f..b9346597b 100644
--- a/config-ui/src/plugins/register/starrocks/config.ts
+++ b/config-ui/src/plugins/register/starrocks/config.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
 
 import { BasePipelineConfig } from '../base';
 
diff --git a/config-ui/src/plugins/register/tapd/config.tsx 
b/config-ui/src/plugins/register/tapd/config.tsx
index cd1fb77ad..1c36f8a8e 100644
--- a/config-ui/src/plugins/register/tapd/config.tsx
+++ b/config-ui/src/plugins/register/tapd/config.tsx
@@ -16,11 +16,10 @@
  *
  */
 
-import React from 'react';
-
 import { ExternalLink } from '@/components';
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/plugins/register/teambition/config.tsx 
b/config-ui/src/plugins/register/teambition/config.tsx
index 5b724b4d5..df4e29bec 100644
--- a/config-ui/src/plugins/register/teambition/config.tsx
+++ b/config-ui/src/plugins/register/teambition/config.tsx
@@ -16,10 +16,8 @@
  *
  */
 
-import React from 'react';
-
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { ConnectionTenantId, ConnectionTenantType } from './connection-fields';
diff --git a/config-ui/src/plugins/register/webook/config.ts 
b/config-ui/src/plugins/register/webook/config.ts
index 06510dceb..4aa4a557b 100644
--- a/config-ui/src/plugins/register/webook/config.ts
+++ b/config-ui/src/plugins/register/webook/config.ts
@@ -16,15 +16,17 @@
  *
  */
 
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { BasePipelineConfig } from '../base';
 
-export const WebhookConfig = {
+export const WebhookConfig: PluginConfigType = {
   ...BasePipelineConfig,
   plugin: 'webhook',
   name: 'Webhook',
   type: PluginType.Connection,
   icon: Icon,
+  sort: 100,
 };
diff --git a/config-ui/src/plugins/register/zentao/config.ts 
b/config-ui/src/plugins/register/zentao/config.ts
index 71b67ea09..27f34b16a 100644
--- a/config-ui/src/plugins/register/zentao/config.ts
+++ b/config-ui/src/plugins/register/zentao/config.ts
@@ -16,8 +16,8 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
-import { PluginType } from '@/plugins';
+import type { PluginConfigType } from '../../types';
+import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
diff --git a/config-ui/src/store/connections/api.ts 
b/config-ui/src/store/connections/api.ts
index e2ff846ef..d84d04852 100644
--- a/config-ui/src/store/connections/api.ts
+++ b/config-ui/src/store/connections/api.ts
@@ -18,7 +18,18 @@
 
 import { request } from '@/utils';
 
-export const getConnection = (plugin: string) => 
request(`/plugins/${plugin}/connections`);
+type GetConnectionRes = {
+  id: ID;
+  name: string;
+  endpoint: string;
+  proxy: string;
+  token?: string;
+  username?: string;
+  password?: string;
+  authMethod?: string;
+};
+
+export const getConnection = (plugin: string): Promise<GetConnectionRes[]> => 
request(`/plugins/${plugin}/connections`);
 
 type TestConnectionPayload = {
   endpoint: string;
diff --git a/config-ui/src/store/connections/context.tsx 
b/config-ui/src/store/connections/context.tsx
index 6054ed823..fcd69c661 100644
--- a/config-ui/src/store/connections/context.tsx
+++ b/config-ui/src/store/connections/context.tsx
@@ -26,8 +26,8 @@ import { useContextValue } from './use-context-value';
 
 const ConnectionContext = React.createContext<{
   connections: ConnectionItemType[];
-  onRefresh: () => void;
-  onTest: (selectedConnection: ConnectionItemType) => void;
+  onRefresh: (plugin?: string) => void;
+  onTest: (unique: string) => void;
 }>({
   connections: [],
   onRefresh: () => {},
diff --git a/config-ui/src/store/connections/status.tsx 
b/config-ui/src/store/connections/status.tsx
index 9a551fa75..fe9681d20 100644
--- a/config-ui/src/store/connections/status.tsx
+++ b/config-ui/src/store/connections/status.tsx
@@ -16,13 +16,10 @@
  *
  */
 
-import { Icon, Colors, Position, Intent } from '@blueprintjs/core';
-import { Tooltip2 } from '@blueprintjs/popover2';
 import styled from 'styled-components';
 
-import { Loading } from '@/components';
+import { IconButton } from '@/components';
 
-import type { ConnectionItemType } from './types';
 import { ConnectionStatusEnum } from './types';
 
 const Wrapper = styled.div`
@@ -30,47 +27,39 @@ const Wrapper = styled.div`
   align-items: center;
 
   & > span.online {
-    color: ${Colors.GREEN3};
+    color: #4db764;
   }
 
   & > span.offline {
-    color: ${Colors.RED3};
-  }
-
-  & > span.testing {
-    color: #7497f7;
+    color: #e34040;
   }
 `;
 
 const STATUS_MAP = {
-  [`${ConnectionStatusEnum.NULL}`]: 'Init',
+  [`${ConnectionStatusEnum.NULL}`]: 'Test',
   [`${ConnectionStatusEnum.TESTING}`]: 'Testing',
-  [`${ConnectionStatusEnum.ONLINE}`]: 'Online',
-  [`${ConnectionStatusEnum.OFFLINE}`]: 'Offline',
+  [`${ConnectionStatusEnum.ONLINE}`]: 'Connected',
+  [`${ConnectionStatusEnum.OFFLINE}`]: 'Disconnected',
 };
 
 interface Props {
-  connection: ConnectionItemType;
-  onTest: (connection: ConnectionItemType) => void;
+  status: ConnectionStatusEnum;
+  unique: string;
+  onTest: (unique: string) => void;
 }
 
-export const ConnectionStatus = ({ connection, onTest }: Props) => {
-  const { status } = connection;
-
+export const ConnectionStatus = ({ status, unique, onTest }: Props) => {
   return (
     <Wrapper>
-      {status === ConnectionStatusEnum.TESTING && <Loading size={14} style={{ 
marginRight: 4 }} />}
-      {status === ConnectionStatusEnum.OFFLINE && (
-        <Tooltip2 intent={Intent.PRIMARY} position={Position.TOP} 
content="Retry">
-          <Icon
-            size={14}
-            icon="repeat"
-            style={{ marginRight: 4, color: Colors.RED3, cursor: 'pointer' }}
-            onClick={() => onTest(connection)}
-          />
-        </Tooltip2>
-      )}
       <span className={status}>{STATUS_MAP[status]}</span>
+      {status !== ConnectionStatusEnum.ONLINE && (
+        <IconButton
+          loading={status === ConnectionStatusEnum.TESTING}
+          icon="repeat"
+          tooltip="Retry"
+          onClick={() => onTest(unique)}
+        />
+      )}
     </Wrapper>
   );
 };
diff --git a/config-ui/src/store/connections/use-context-value.ts 
b/config-ui/src/store/connections/use-context-value.ts
index 369bc8a92..72eeeab18 100644
--- a/config-ui/src/store/connections/use-context-value.ts
+++ b/config-ui/src/store/connections/use-context-value.ts
@@ -16,8 +16,9 @@
  *
  */
 
-import { useState, useEffect, useCallback, useMemo } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 
+import type { PluginConfigType } from '@/plugins';
 import { PluginConfig, PluginType } from '@/plugins';
 
 import type { ConnectionItemType } from './types';
@@ -31,114 +32,122 @@ export interface UseContextValueProps {
   filter?: string[];
 }
 
-export const useContextValue = ({ plugin, filterBeta = false, filterPlugin, 
filter }: UseContextValueProps) => {
-  const [loading, setLoading] = useState(false);
+export const useContextValue = ({ plugin, filterBeta, filterPlugin, filter }: 
UseContextValueProps) => {
+  const [loading, setLoading] = useState(true);
   const [connections, setConnections] = useState<ConnectionItemType[]>([]);
 
-  const allConnections = useMemo(
+  const plugins = useMemo(
     () =>
       PluginConfig.filter((p) => p.type === PluginType.Connection)
+        .filter((p) => (plugin ? p.plugin === plugin : true))
         .filter((p) => (filterBeta ? !p.isBeta : true))
-        .filter((p) => (filterPlugin ? !filterPlugin.includes(p.plugin) : 
true))
-        .filter((p) => (plugin ? p.plugin === plugin : true)),
+        .filter((p) => (filterPlugin ? !filterPlugin.includes(p.plugin) : 
true)),
     [plugin],
   );
 
   const getConnection = async (plugin: string) => {
     try {
-      return await API.getConnection(plugin);
+      const res = await API.getConnection(plugin);
+      const { icon, entities } = plugins.find((p) => p.plugin === plugin) as 
PluginConfigType;
+
+      return res.map((connection) => ({
+        ...connection,
+        plugin,
+        icon,
+        entities,
+      }));
     } catch {
       return [];
     }
   };
 
-  const handleRefresh = useCallback(async () => {
-    setLoading(true);
+  const testConnection = async ({
+    plugin,
+    endpoint,
+    proxy,
+    token,
+    username,
+    password,
+    authMethod,
+  }: ConnectionItemType) => {
+    try {
+      const res = await API.testConnection(plugin, {
+        endpoint,
+        proxy,
+        token,
+        username,
+        password,
+        authMethod,
+      });
+      return res.success ? ConnectionStatusEnum.ONLINE : 
ConnectionStatusEnum.OFFLINE;
+    } catch {
+      return ConnectionStatusEnum.OFFLINE;
+    }
+  };
 
-    const res = await Promise.all(allConnections.map((cs) => 
getConnection(cs.plugin)));
+  const transformConnection = (connections: Omit<ConnectionItemType, 'unique' 
| 'status'>[]) => {
+    return connections.map((it) => ({
+      unique: `${it.plugin}-${it.id}`,
+      status: ConnectionStatusEnum.NULL,
+      plugin: it.plugin,
+      id: it.id,
+      name: it.name,
+      icon: it.icon,
+      entities: it.entities,
+      endpoint: it.endpoint,
+      proxy: it.proxy,
+      token: it.token,
+      username: it.username,
+      password: it.password,
+      authMethod: it.authMethod,
+    }));
+  };
+
+  const handleRefresh = async (plugin?: string) => {
+    if (plugin) {
+      const res = await getConnection(plugin);
+      setConnections([...connections.filter((cs) => cs.plugin !== plugin), 
...transformConnection(res)]);
+      return;
+    }
 
-    const resWithPlugin = res.map((cs, i) =>
-      cs.map((it: any) => {
-        const { plugin, icon, entities } = allConnections[i];
+    const res = await Promise.all(plugins.map((cs) => 
getConnection(cs.plugin)));
 
-        return {
-          ...it,
-          plugin,
-          icon,
-          entities,
-        };
-      }),
-    );
+    setConnections(transformConnection(res.flat()));
+    setLoading(false);
+  };
 
-    setConnections(
-      resWithPlugin.flat().map((it) => ({
-        unique: `${it.plugin}-${it.id}`,
-        status: ConnectionStatusEnum.NULL,
-        plugin: it.plugin,
-        id: it.id,
-        name: it.name,
-        icon: it.icon,
-        entities: it.entities,
-        endpoint: it.endpoint,
-        proxy: it.proxy,
-        token: it.token,
-        username: it.username,
-        password: it.password,
-        authMethod: it.authMethod,
-      })),
+  const handleTest = async (unique: string) => {
+    setConnections((connections) =>
+      connections.map((cs) =>
+        cs.unique === unique
+          ? {
+              ...cs,
+              status: ConnectionStatusEnum.TESTING,
+            }
+          : cs,
+      ),
     );
 
-    setLoading(false);
-  }, [allConnections]);
+    console.log(connections);
+
+    const connection = connections.find((cs) => cs.unique === unique) as 
ConnectionItemType;
+    const status = await testConnection(connection);
+
+    setConnections((connections) =>
+      connections.map((cs) =>
+        cs.unique === unique
+          ? {
+              ...cs,
+              status,
+            }
+          : cs,
+      ),
+    );
+  };
 
   useEffect(() => {
     handleRefresh();
-  }, [allConnections]);
-
-  const handleTest = useCallback(
-    async (selectedConnection: ConnectionItemType) => {
-      setConnections((connections) =>
-        connections.map((cs) =>
-          cs.unique === selectedConnection.unique
-            ? {
-                ...cs,
-                status: ConnectionStatusEnum.TESTING,
-              }
-            : cs,
-        ),
-      );
-
-      const { plugin, endpoint, proxy, token, username, password, authMethod } 
= selectedConnection;
-
-      let status = ConnectionStatusEnum.OFFLINE;
-
-      try {
-        const res = await API.testConnection(plugin, {
-          endpoint,
-          proxy,
-          token,
-          username,
-          password,
-          authMethod,
-        });
-        status = res.success ? ConnectionStatusEnum.ONLINE : 
ConnectionStatusEnum.OFFLINE;
-      } catch {
-        status = ConnectionStatusEnum.OFFLINE;
-      }
-
-      setConnections((connections) =>
-        connections.map((cs) =>
-          cs.unique === selectedConnection.unique
-            ? {
-                ...cs,
-                status,
-              }
-            : cs,
-        ),
-      );
-    },
-    [connections],
-  );
+  }, []);
 
   return useMemo(
     () => ({

Reply via email to