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

[incubator-devlake] branch main updated: fix(config-ui): missed some code for webhook connection (#6311)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new a0821bb94 fix(config-ui): missed some code for webhook connection (#6311)
a0821bb94 is described below

commit a0821bb94e271271fca3c01367679b22b510e1c1
Author: 青湛 <0x...@gmail.com>
AuthorDate: Wed Oct 25 18:21:49 2023 +1300

    fix(config-ui): missed some code for webhook connection (#6311)
---
 config-ui/src/api/plugin/webhook/index.ts          |  20 +---
 config-ui/src/features/connections/slice.ts        | 117 +++++++++++++++------
 config-ui/src/features/connections/utils.ts        |  14 ++-
 config-ui/src/pages/connection/home/index.tsx      |  41 +++-----
 .../register/webhook/components/create-dialog.tsx  |  14 ++-
 .../register/webhook/components/delete-dialog.tsx  |   7 +-
 .../register/webhook/components/edit-dialog.tsx    |  15 +--
 .../webhook/components/selector-dialog.tsx         |  56 ++++------
 .../register/webhook/components/view-dialog.tsx    |  95 +++++++----------
 .../src/plugins/register/webhook/connection.tsx    |  12 +--
 config-ui/src/types/index.ts                       |   2 +
 config-ui/src/types/{index.ts => status.ts}        |   2 +-
 config-ui/src/types/{index.ts => webhook.ts}       |  22 +++-
 13 files changed, 224 insertions(+), 193 deletions(-)

diff --git a/config-ui/src/api/plugin/webhook/index.ts b/config-ui/src/api/plugin/webhook/index.ts
index 27155d23a..18c5c6621 100644
--- a/config-ui/src/api/plugin/webhook/index.ts
+++ b/config-ui/src/api/plugin/webhook/index.ts
@@ -18,23 +18,13 @@
 
 import * as connection from '../../connection';
 
-export const list = () => connection.list('webhook');
+import { IApiWebhook } from '@/types';
 
-export const get = (
-  id: ID,
-): Promise<{
-  name: string;
-  postIssuesEndpoint: string;
-  closeIssuesEndpoint: string;
-  postPipelineDeployTaskEndpoint: string;
-  apiKey: {
-    id: string;
-    apiKey: string;
-  };
-}> => connection.get('webhook', id) as any;
+export const list = (): Promise<IApiWebhook[]> => connection.list('webhook') as any;
 
-export const create = (payload: any): Promise<{ id: string; apiKey: { apiKey: string } }> =>
-  connection.create('webhook', payload) as any;
+export const get = (id: ID): Promise<IApiWebhook> => connection.get('webhook', id) as any;
+
+export const create = (payload: any): Promise<IApiWebhook> => connection.create('webhook', payload) as any;
 
 export const remove = (id: ID) => connection.remove('webhook', id);
 
diff --git a/config-ui/src/features/connections/slice.ts b/config-ui/src/features/connections/slice.ts
index 935379bff..60567509a 100644
--- a/config-ui/src/features/connections/slice.ts
+++ b/config-ui/src/features/connections/slice.ts
@@ -22,15 +22,17 @@ import { flatten } from 'lodash';
 import API from '@/api';
 import { RootState } from '@/app/store';
 import { PluginConfig } from '@/plugins';
-import { IConnection, IConnectionStatus } from '@/types';
+import { IConnection, IConnectionStatus, IWebhook, IStatus } from '@/types';
 
-import { transformConnection } from './utils';
+import { transformConnection, transformWebhook } from './utils';
 
 const initialState: {
   connections: IConnection[];
-  status: 'idle' | 'loading' | 'success' | 'failed';
+  webhooks: IWebhook[];
+  status: IStatus;
 } = {
   connections: [],
+  webhooks: [],
   status: 'idle',
 };
 
@@ -43,23 +45,47 @@ export const init = createAsyncThunk('connections/init', async () => {
     }
   };
 
-  const res = await Promise.all(
-    PluginConfig.map(async ({ plugin }) => {
+  const connections = await Promise.all(
+    PluginConfig.filter((p) => p.plugin !== 'webhook').map(async ({ plugin }) => {
       const connections = await getConnections(plugin);
       return connections.map((connection) => transformConnection(plugin, connection));
     }),
   );
-  return flatten(res);
-});
 
-export const fetchConnections = createAsyncThunk('connections/fetchConnections', async (plugin: string) => {
-  const connections = await API.connection.list(plugin);
+  const webhooks = await Promise.all(
+    PluginConfig.filter((p) => p.plugin === 'webhook').map(async () => {
+      const webhooks = await API.plugin.webhook.list();
+      return webhooks.map((webhook) => transformWebhook(webhook));
+    }),
+  );
+
   return {
-    plugin,
-    connections: connections.map((connection) => transformConnection(plugin, connection)),
+    connections: flatten(connections),
+    webhooks: flatten(webhooks),
   };
 });
 
+export const addConnection = createAsyncThunk('connections/addConnection', async ({ plugin, ...payload }: any) => {
+  const connection = await API.connection.create(plugin, payload);
+  return transformConnection(plugin, connection);
+});
+
+export const updateConnection = createAsyncThunk(
+  'connections/updateConnection',
+  async ({ plugin, connectionId, ...payload }: any) => {
+    const connection = await API.connection.update(plugin, connectionId, payload);
+    return transformConnection(plugin, connection);
+  },
+);
+
+export const removeConnection = createAsyncThunk(
+  'connections/removeConnection',
+  async ({ plugin, connectionId }: any) => {
+    await API.connection.remove(plugin, connectionId);
+    return `${plugin}-${connectionId}`;
+  },
+);
+
 export const testConnection = createAsyncThunk(
   'connections/testConnection',
   async ({ unique, plugin, endpoint, proxy, token, username, password, authMethod, secretKey, appId }: IConnection) => {
@@ -81,28 +107,31 @@ export const testConnection = createAsyncThunk(
   },
 );
 
-export const addConnection = createAsyncThunk('connections/addConnection', async ({ plugin, ...payload }: any) => {
-  const connection = await API.connection.create(plugin, payload);
-  return transformConnection(plugin, connection);
+export const addWebhook = createAsyncThunk('connections/addWebhook', async (payload: any) => {
+  const webhook = await API.plugin.webhook.create(payload);
+  return transformWebhook(webhook);
 });
 
-export const updateConnection = createAsyncThunk(
-  'connections/updateConnection',
-  async ({ plugin, connectionId, ...payload }: any) => {
-    const connection = await API.connection.update(plugin, connectionId, payload);
-    return transformConnection(plugin, connection);
-  },
-);
+export const removeWebhook = createAsyncThunk('connections/removeWebhook', async (id: ID) => {
+  await API.plugin.webhook.remove(id);
+  return id;
+});
 
-export const removeConnection = createAsyncThunk(
-  'connections/removeConnection',
-  async ({ plugin, connectionId }: any) => {
-    await API.connection.remove(plugin, connectionId);
-    return `${plugin}-${connectionId}`;
-  },
-);
+export const updateWebhook = createAsyncThunk('connections/updateWebhook', async ({ id, ...payload }: any) => {
+  const webhook = await API.plugin.webhook.update(id, payload);
+  return webhook;
+});
 
-export const slice = createSlice({
+export const renewWebhookApiKey = createAsyncThunk('connections/renewWebhookApiKey', async (id: ID, { getState }) => {
+  const webhook = (getState() as RootState).connections.webhooks.find((wh) => wh.id === id) as IWebhook;
+  const apiKey = await API.apiKey.renew(webhook.apiKeyId);
+  return {
+    id: webhook.id,
+    apiKey: apiKey.apiKey,
+  };
+});
+
+export const ConnectionsSlice = createSlice({
   name: 'connections',
   initialState,
   reducers: {},
@@ -112,12 +141,10 @@ export const slice = createSlice({
         state.status = 'loading';
       })
       .addCase(init.fulfilled, (state, action) => {
-        state.connections = action.payload;
+        state.connections = action.payload.connections;
+        state.webhooks = action.payload.webhooks;
         state.status = 'success';
       })
-      .addCase(fetchConnections.fulfilled, (state, action) => {
-        state.connections = state.connections.concat(action.payload.connections);
-      })
       .addCase(addConnection.fulfilled, (state, action) => {
         state.connections.push(action.payload);
       })
@@ -143,13 +170,29 @@ export const slice = createSlice({
         if (existingConnection) {
           existingConnection.status = action.payload.status;
         }
+      })
+      .addCase(addWebhook.fulfilled, (state, action) => {
+        state.webhooks.push(action.payload);
+      })
+      .addCase(removeWebhook.fulfilled, (state, action) => {
+        state.webhooks = state.webhooks.filter((wh) => wh.id !== action.payload);
+      })
+      .addCase(updateWebhook.fulfilled, (state, action) => {
+        state.webhooks = state.webhooks.map((wh) =>
+          wh.id === action.payload.id ? { ...wh, name: action.payload.name } : wh,
+        );
+      })
+      .addCase(renewWebhookApiKey.fulfilled, (state, action) => {
+        state.webhooks = state.webhooks.map((wh) =>
+          wh.id === action.payload.id ? { ...wh, apiKey: action.payload.apiKey } : wh,
+        );
       });
   },
 });
 
-export const {} = slice.actions;
+export const {} = ConnectionsSlice.actions;
 
-export default slice.reducer;
+export default ConnectionsSlice.reducer;
 
 export const selectStatus = (state: RootState) => state.connections.status;
 
@@ -160,3 +203,7 @@ export const selectConnections = (state: RootState, plugin: string) =>
 
 export const selectConnection = (state: RootState, unique: string) =>
   state.connections.connections.find((cs) => cs.unique === unique);
+
+export const selectWebhooks = (state: RootState) => state.connections.webhooks;
+
+export const selectWebhook = (state: RootState, id: ID) => state.connections.webhooks.find((wh) => wh.id === id);
diff --git a/config-ui/src/features/connections/utils.ts b/config-ui/src/features/connections/utils.ts
index 00161a4ca..1de94647d 100644
--- a/config-ui/src/features/connections/utils.ts
+++ b/config-ui/src/features/connections/utils.ts
@@ -20,7 +20,7 @@ import * as T from '@/api/connection/types';
 import type { PluginConfigType } from '@/plugins';
 import { PluginConfig } from '@/plugins';
 
-import { IConnection, IConnectionStatus } from '@/types';
+import { IConnection, IConnectionStatus, IApiWebhook, IWebhook } from '@/types';
 
 export const transformConnection = (plugin: string, connection: T.Connection): IConnection => {
   const config = PluginConfig.find((p) => p.plugin === plugin) as PluginConfigType;
@@ -43,3 +43,15 @@ export const transformConnection = (plugin: string, connection: T.Connection): I
     secretKey: connection.secretKey,
   };
 };
+
+export const transformWebhook = (connection: IApiWebhook): IWebhook => {
+  return {
+    id: connection.id,
+    name: connection.name,
+    postIssuesEndpoint: connection.postIssuesEndpoint,
+    closeIssuesEndpoint: connection.closeIssuesEndpoint,
+    postPipelineDeployTaskEndpoint: connection.postPipelineDeployTaskEndpoint,
+    apiKey: connection.apiKey.apiKey,
+    apiKeyId: connection.apiKey.id,
+  };
+};
diff --git a/config-ui/src/pages/connection/home/index.tsx b/config-ui/src/pages/connection/home/index.tsx
index 5df3a6ad6..bbe948637 100644
--- a/config-ui/src/pages/connection/home/index.tsx
+++ b/config-ui/src/pages/connection/home/index.tsx
@@ -16,13 +16,13 @@
  *
  */
 
-import { useState, useMemo } from 'react';
+import { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { Tag, Intent } from '@blueprintjs/core';
 
 import { useAppSelector } from '@/app/hook';
 import { Dialog } from '@/components';
-import { selectAllConnections } from '@/features/connections';
+import { selectAllConnections, selectWebhooks } from '@/features/connections';
 import { PluginConfig, PluginConfigType, ConnectionList, ConnectionForm } from '@/plugins';
 
 import * as S from './styled';
@@ -32,18 +32,10 @@ export const ConnectionHomePage = () => {
   const [pluginConfig, setPluginConfig] = useState<PluginConfigType>();
 
   const connections = useAppSelector(selectAllConnections);
+  const webhooks = useAppSelector(selectWebhooks);
 
   const navigate = useNavigate();
 
-  const plugins = useMemo(
-    () =>
-      PluginConfig.map((p) => ({
-        ...p,
-        count: connections.filter((cs) => cs.plugin === p.plugin).length,
-      })),
-    [connections],
-  );
-
   const handleShowListDialog = (pluginConfig: PluginConfigType) => {
     setType('list');
     setPluginConfig(pluginConfig);
@@ -77,20 +69,21 @@ export const ConnectionHomePage = () => {
           You can create and manage data connections for the following data sources and use them in your Projects.
         </h5>
         <ul>
-          {plugins
-            .filter((p) => p.plugin !== 'webhook')
-            .map((p) => (
+          {PluginConfig.filter((p) => p.plugin !== 'webhook').map((p) => {
+            const connectionCount = connections.filter((cs) => cs.plugin === p.plugin).length;
+            return (
               <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>
+                <S.Count>{connectionCount ? `${connectionCount} connections` : 'No connection'}</S.Count>
                 {p.isBeta && (
                   <Tag intent={Intent.WARNING} round>
                     beta
                   </Tag>
                 )}
               </li>
-            ))}
+            );
+          })}
         </ul>
       </div>
       <div className="block">
@@ -100,20 +93,16 @@ export const ConnectionHomePage = () => {
           DORA metrics, etc.
         </h5>
         <ul>
-          {plugins
-            .filter((p) => p.plugin === 'webhook')
-            .map((p) => (
+          {PluginConfig.filter((p) => p.plugin === 'webhook').map((p) => {
+            const webhookCount = webhooks.length;
+            return (
               <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>
-                )}
+                <S.Count>{webhookCount ? `${webhookCount} connections` : 'No connection'}</S.Count>
               </li>
-            ))}
+            );
+          })}
         </ul>
       </div>
       {type === 'list' && pluginConfig && (
diff --git a/config-ui/src/plugins/register/webhook/components/create-dialog.tsx b/config-ui/src/plugins/register/webhook/components/create-dialog.tsx
index e3be58407..6f45f6abf 100644
--- a/config-ui/src/plugins/register/webhook/components/create-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/create-dialog.tsx
@@ -19,8 +19,9 @@
 import { useState, useMemo } from 'react';
 import { InputGroup, Icon } from '@blueprintjs/core';
 
-import API from '@/api';
+import { useAppDispatch } from '@/app/hook';
 import { Dialog, FormItem, CopyText, ExternalLink } from '@/components';
+import { addWebhook } from '@/features';
 import { operator } from '@/utils';
 
 import * as S from '../styled';
@@ -43,17 +44,20 @@ export const CreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props) => {
     apiKey: '',
   });
 
+  const dispatch = useAppDispatch();
+
   const prefix = useMemo(() => `${window.location.origin}/api`, []);
 
   const handleSubmit = async () => {
     const [success, res] = await operator(
       async () => {
-        const { id, apiKey } = await API.plugin.webhook.create({ name });
-        const { postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint } =
-          await API.plugin.webhook.get(id);
+        const { id, apiKey, postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint } = await dispatch(
+          addWebhook({ name }),
+        ).unwrap();
+
         return {
           id,
-          apiKey: apiKey.apiKey,
+          apiKey,
           postIssuesEndpoint,
           closeIssuesEndpoint,
           postPipelineDeployTaskEndpoint,
diff --git a/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx b/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx
index 806a26d5e..a5f298f89 100644
--- a/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx
@@ -18,8 +18,9 @@
 
 import { useState } from 'react';
 
-import API from '@/api';
+import { useAppDispatch } from '@/app/hook';
 import { Dialog, Message } from '@/components';
+import { removeWebhook } from '@/features';
 import { operator } from '@/utils';
 
 interface Props {
@@ -31,8 +32,10 @@ interface Props {
 export const DeleteDialog = ({ initialId, onCancel, onSubmitAfter }: Props) => {
   const [operating, setOperating] = useState(false);
 
+  const dispatch = useAppDispatch();
+
   const handleSubmit = async () => {
-    const [success] = await operator(() => API.plugin.webhook.remove(initialId), {
+    const [success] = await operator(() => dispatch(removeWebhook(initialId)), {
       setOperating,
     });
 
diff --git a/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx b/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx
index f656cdf7c..39e2ba3db 100644
--- a/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx
@@ -19,8 +19,9 @@
 import { useState, useEffect } from 'react';
 import { InputGroup } from '@blueprintjs/core';
 
-import API from '@/api';
+import { useAppDispatch, useAppSelector } from '@/app/hook';
 import { Dialog, FormItem } from '@/components';
+import { updateWebhook, selectWebhook } from '@/features';
 import { operator } from '@/utils';
 
 interface Props {
@@ -32,15 +33,15 @@ export const EditDialog = ({ initialId, onCancel }: Props) => {
   const [name, setName] = useState('');
   const [operating, setOperating] = useState(false);
 
+  const dispatch = useAppDispatch();
+  const webhook = useAppSelector((state) => selectWebhook(state, initialId));
+
   useEffect(() => {
-    (async () => {
-      const res = await API.plugin.webhook.get(initialId);
-      setName(res.name);
-    })();
-  }, [initialId]);
+    setName(webhook?.name ?? '');
+  }, [webhook]);
 
   const handleSubmit = async () => {
-    const [success] = await operator(() => API.plugin.webhook.update(initialId, { name }), {
+    const [success] = await operator(() => dispatch(updateWebhook({ id: initialId, name })), {
       setOperating,
     });
 
diff --git a/config-ui/src/plugins/register/webhook/components/selector-dialog.tsx b/config-ui/src/plugins/register/webhook/components/selector-dialog.tsx
index e3ac67c3b..49f900eee 100644
--- a/config-ui/src/plugins/register/webhook/components/selector-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/selector-dialog.tsx
@@ -16,45 +16,29 @@
  *
  */
 
-import { useState, useEffect } from 'react';
-import type { McsItem } from 'miller-columns-select';
+import { useState } from 'react';
 import MillerColumnsSelect from 'miller-columns-select';
 
-import API from '@/api';
+import { useAppSelector } from '@/app/hook';
 import { Dialog, FormItem, Loading } from '@/components';
+import { selectWebhooks } from '@/features';
+import { IWebhook } from '@/types';
 
-import * as T from '../types';
 import * as S from '../styled';
 
 interface Props {
   isOpen: boolean;
   saving: boolean;
   onCancel: () => void;
-  onSubmit: (items: T.WebhookItemType[]) => void;
+  onSubmit: (items: IWebhook[]) => void;
 }
 
 export const SelectorDialog = ({ isOpen, saving, onCancel, onSubmit }: Props) => {
-  const [items, setItems] = useState<McsItem<T.WebhookItemType>[]>([]);
-  const [selectedItems, setSelectedItems] = useState<T.WebhookItemType[]>([]);
-  const [isLast, setIsLast] = useState(false);
+  const [selectedIds, setSelectedIds] = useState<ID[]>([]);
 
-  const updateItems = (arr: any) =>
-    arr.map((it: any) => ({
-      parentId: null,
-      id: it.id,
-      title: it.name,
-      name: it.name,
-    }));
+  const webhooks = useAppSelector(selectWebhooks);
 
-  useEffect(() => {
-    (async () => {
-      const res = await API.plugin.webhook.list();
-      setItems([...updateItems(res)]);
-      setIsLast(true);
-    })();
-  }, []);
-
-  const handleSubmit = () => onSubmit(selectedItems);
+  const handleSubmit = () => onSubmit(webhooks.filter((it) => selectedIds.includes(it.id)));
 
   return (
     <Dialog
@@ -65,7 +49,7 @@ export const SelectorDialog = ({ isOpen, saving, onCancel, onSubmit }: Props) =>
       }}
       okText="Confrim"
       okLoading={saving}
-      okDisabled={!selectedItems.length}
+      okDisabled={!selectedIds.length}
       onCancel={onCancel}
       onOk={handleSubmit}
     >
@@ -74,20 +58,16 @@ export const SelectorDialog = ({ isOpen, saving, onCancel, onSubmit }: Props) =>
           <MillerColumnsSelect
             columnCount={1}
             columnHeight={160}
-            getHasMore={() => !isLast}
+            getHasMore={() => false}
             renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />}
-            items={items}
-            selectedIds={selectedItems.map((it) => it.id)}
-            onSelectItemIds={(seletedIds: ID[]) =>
-              setSelectedItems(
-                items
-                  .filter((it) => seletedIds.includes(it.id))
-                  .map((it) => ({
-                    id: it.id,
-                    name: it.name,
-                  })),
-              )
-            }
+            items={webhooks.map((it) => ({
+              parentId: null,
+              id: it.id,
+              title: it.name,
+              name: it.name,
+            }))}
+            selectedIds={selectedIds}
+            onSelectItemIds={setSelectedIds}
           />
         </FormItem>
       </S.Wrapper>
diff --git a/config-ui/src/plugins/register/webhook/components/view-dialog.tsx b/config-ui/src/plugins/register/webhook/components/view-dialog.tsx
index 263d0e508..e3c89a36e 100644
--- a/config-ui/src/plugins/register/webhook/components/view-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/view-dialog.tsx
@@ -16,11 +16,13 @@
  *
  */
 
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useMemo } from 'react';
 import { Button, Intent } from '@blueprintjs/core';
 
-import API from '@/api';
+import { useAppDispatch, useAppSelector } from '@/app/hook';
 import { Dialog, FormItem, CopyText, ExternalLink, Message } from '@/components';
+import { selectWebhook, renewWebhookApiKey } from '@/features';
+import { IWebhook } from '@/types';
 import { operator } from '@/utils';
 
 import * as S from '../styled';
@@ -30,68 +32,49 @@ interface Props {
   onCancel: () => void;
 }
 
+const transformURI = (prefix: string, webhook: IWebhook) => {
+  return {
+    postIssuesEndpoint: `curl ${prefix}${webhook.postIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${
+      webhook.apiKey ?? '{API_KEY}'
+    }' -d '{
+        "issue_key":"DLK-1234",
+        "title":"a feature from DLK",
+        "type":"INCIDENT",
+        "original_status":"TODO",
+        "status":"TODO",    
+        "created_date":"2020-01-01T12:00:00+00:00",
+        "updated_date":"2020-01-01T12:00:00+00:00"
+     }'`,
+    closeIssuesEndpoint: `curl ${prefix}${webhook.closeIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${
+      webhook.apiKey ?? '{API_KEY}'
+    }'`,
+    postDeploymentsCurl: `curl ${prefix}${webhook.postPipelineDeployTaskEndpoint} -X 'POST' -H 'Authorization: Bearer ${
+      webhook.apiKey ?? '{API_KEY}'
+    }' -d '{
+         "commit_sha":"the sha of deployment commit",
+         "repo_url":"the repo URL of the deployment commit",
+         "start_time":"Optional, eg. 2020-01-01T12:00:00+00:00"
+     }'`,
+  };
+};
+
 export const ViewDialog = ({ initialId, onCancel }: Props) => {
-  const [record, setRecord] = useState({
-    apiKeyId: '',
-    postIssuesEndpoint: '',
-    closeIssuesEndpoint: '',
-    postDeploymentsCurl: '',
-  });
   const [isOpen, setIsOpen] = useState(false);
   const [operating, setOperating] = useState(false);
-  const [apiKey, setApiKey] = useState('');
 
+  const dispatch = useAppDispatch();
+  const webhook = useAppSelector((state) => selectWebhook(state, initialId)) as IWebhook;
   const prefix = useMemo(() => `${window.location.origin}/api`, []);
 
-  useEffect(() => {
-    (async () => {
-      const res = await API.plugin.webhook.get(initialId);
-      setRecord({
-        apiKeyId: res.apiKey.id,
-        postIssuesEndpoint: ` curl ${prefix}${res.postIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer {API_KEY}' -d '{
-          "issue_key":"DLK-1234",
-          "title":"a feature from DLK",
-          "type":"INCIDENT",
-          "original_status":"TODO",
-          "status":"TODO",    
-          "created_date":"2020-01-01T12:00:00+00:00",
-          "updated_date":"2020-01-01T12:00:00+00:00"
-       }'`,
-        closeIssuesEndpoint: `curl ${prefix}${res.closeIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer {API_KEY}'`,
-        postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST' -H 'Authorization: Bearer {API_KEY}' -d '{
-           "commit_sha":"the sha of deployment commit",
-           "repo_url":"the repo URL of the deployment commit",
-           "start_time":"Optional, eg. 2020-01-01T12:00:00+00:00"
-       }'`,
-      });
-    })();
-  }, [initialId]);
+  const URI = transformURI(prefix, webhook);
 
   const handleGenerateNewKey = async () => {
-    const [success, res] = await operator(() => API.apiKey.renew(record.apiKeyId), {
+    const [success] = await operator(() => dispatch(renewWebhookApiKey(initialId)), {
       setOperating,
     });
 
     if (success) {
       setIsOpen(false);
-      setApiKey(res.apiKey);
-      setRecord({
-        ...record,
-        postIssuesEndpoint: ` curl ${record.postIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${res.apiKey}' -d '{
-          "issue_key":"DLK-1234",
-          "title":"a feature from DLK",
-          "type":"INCIDENT",
-          "status":"TODO",    
-          "created_date":"2020-01-01T12:00:00+00:00",
-          "updated_date":"2020-01-01T12:00:00+00:00"
-       }'`,
-        closeIssuesEndpoint: `curl ${record.closeIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${res.apiKey}'`,
-        postDeploymentsCurl: `curl ${record.postDeploymentsCurl} -X 'POST' -H 'Authorization: Bearer ${res.apiKey}' -d '{
-           "commit_sha":"the sha of deployment commit",
-           "repo_url":"the repo URL of the deployment commit",
-           "start_time":"Optional, eg. 2020-01-01T12:00:00+00:00"
-       }'`,
-      });
     }
   };
 
@@ -104,7 +87,7 @@ export const ViewDialog = ({ initialId, onCancel }: Props) => {
         </p>
         <FormItem label="Incident">
           <h5>Post to register/update an incident</h5>
-          <CopyText content={record.postIssuesEndpoint} />
+          <CopyText content={URI.postIssuesEndpoint} />
           <p>
             See the{' '}
             <ExternalLink link="https://devlake.apache.org/docs/Plugins/webhook#register-issues---update-or-create-issues">
@@ -113,7 +96,7 @@ export const ViewDialog = ({ initialId, onCancel }: Props) => {
             .
           </p>
           <h5>Post to close a registered incident</h5>
-          <CopyText content={record.closeIssuesEndpoint} />
+          <CopyText content={URI.closeIssuesEndpoint} />
           <p>
             See the{' '}
             <ExternalLink link="https://devlake.apache.org/docs/Plugins/webhook#register-issues---close-issues-optional">
@@ -124,7 +107,7 @@ export const ViewDialog = ({ initialId, onCancel }: Props) => {
         </FormItem>
         <FormItem label="Deployments">
           <h5>Post to register a deployment</h5>
-          <CopyText content={record.postDeploymentsCurl} />
+          <CopyText content={URI.postDeploymentsCurl} />
           <p>
             See the{' '}
             <ExternalLink link="https://devlake.apache.org/docs/Plugins/webhook#deployment">
@@ -137,12 +120,12 @@ export const ViewDialog = ({ initialId, onCancel }: Props) => {
           label="API Key"
           subLabel="If you have forgotten your API key, you can revoke the previous key and generate a new one as a replacement."
         >
-          {!apiKey ? (
+          {!webhook.apiKey ? (
             <Button intent={Intent.PRIMARY} text="Revoke and generate a new key" onClick={() => setIsOpen(true)} />
           ) : (
             <>
               <S.ApiKey>
-                <CopyText content={apiKey} />
+                <CopyText content={webhook.apiKey} />
                 <span>No Expiration</span>
               </S.ApiKey>
               <S.Tips>
diff --git a/config-ui/src/plugins/register/webhook/connection.tsx b/config-ui/src/plugins/register/webhook/connection.tsx
index 07e68224c..2a6f1a011 100644
--- a/config-ui/src/plugins/register/webhook/connection.tsx
+++ b/config-ui/src/plugins/register/webhook/connection.tsx
@@ -21,11 +21,11 @@ import { Button, Intent } from '@blueprintjs/core';
 
 import { useAppSelector } from '@/app/hook';
 import { Buttons, Table, ColumnType, ExternalLink, IconButton } from '@/components';
-import { selectConnections } from '@/features/connections';
+import { selectWebhooks } from '@/features/connections';
 import { DOC_URL } from '@/release';
+import { IWebhook } from '@/types';
 
 import { CreateDialog, ViewDialog, EditDialog, DeleteDialog } from './components';
-import * as T from './types';
 
 import * as S from './styled';
 
@@ -41,19 +41,19 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
   const [type, setType] = useState<Type>();
   const [currentID, setCurrentID] = useState<ID>();
 
-  const connections = useAppSelector((state) => selectConnections(state, 'webhook'));
+  const webhooks = useAppSelector(selectWebhooks);
 
   const handleHideDialog = () => {
     setType(undefined);
     setCurrentID(undefined);
   };
 
-  const handleShowDialog = (t: Type, r?: T.WebhookItemType) => {
+  const handleShowDialog = (t: Type, r?: IWebhook) => {
     setType(t);
     setCurrentID(r?.id);
   };
 
-  const columns: ColumnType<T.WebhookItemType> = [
+  const columns: ColumnType<IWebhook> = [
     {
       title: 'ID',
       dataIndex: 'id',
@@ -87,7 +87,7 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
       </Buttons>
       <Table
         columns={columns}
-        dataSource={connections.filter((cs) => (filterIds ? filterIds.includes(cs.id) : true))}
+        dataSource={webhooks.filter((cs) => (filterIds ? filterIds.includes(cs.id) : true))}
         noData={{
           text: (
             <>
diff --git a/config-ui/src/types/index.ts b/config-ui/src/types/index.ts
index fcc98feb8..c51a9b914 100644
--- a/config-ui/src/types/index.ts
+++ b/config-ui/src/types/index.ts
@@ -17,3 +17,5 @@
  */
 
 export * from './connection';
+export * from './status';
+export * from './webhook';
diff --git a/config-ui/src/types/index.ts b/config-ui/src/types/status.ts
similarity index 92%
copy from config-ui/src/types/index.ts
copy to config-ui/src/types/status.ts
index fcc98feb8..a363098eb 100644
--- a/config-ui/src/types/index.ts
+++ b/config-ui/src/types/status.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './connection';
+export type IStatus = 'idle' | 'loading' | 'success' | 'failed';
diff --git a/config-ui/src/types/index.ts b/config-ui/src/types/webhook.ts
similarity index 65%
copy from config-ui/src/types/index.ts
copy to config-ui/src/types/webhook.ts
index fcc98feb8..5255f1e6f 100644
--- a/config-ui/src/types/index.ts
+++ b/config-ui/src/types/webhook.ts
@@ -16,4 +16,24 @@
  *
  */
 
-export * from './connection';
+export interface IApiWebhook {
+  id: number;
+  name: string;
+  postIssuesEndpoint: string;
+  closeIssuesEndpoint: string;
+  postPipelineDeployTaskEndpoint: string;
+  apiKey: {
+    id: number;
+    apiKey: string;
+  };
+}
+
+export interface IWebhook {
+  id: number;
+  name: string;
+  postIssuesEndpoint: string;
+  closeIssuesEndpoint: string;
+  postPipelineDeployTaskEndpoint: string;
+  apiKey: string;
+  apiKeyId: number;
+}