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 b2e83943e refactor(config-ui): use redux to reorganize connection data 
(#6283)
b2e83943e is described below

commit b2e83943ef7585b66c2ca2672b6cffa0c055afc4
Author: 青湛 <[email protected]>
AuthorDate: Thu Oct 19 21:10:33 2023 +1300

    refactor(config-ui): use redux to reorganize connection data (#6283)
    
    * refactor(config-ui): remove connections context
    
    * refactor(config-ui): use redux to create connections store
    
    * refactor(config-ui): remove field connection type from connection
    
    * fix(config-ui): lint error
---
 config-ui/package.json                             |   3 +
 config-ui/src/api/connection/types.ts              |   2 +
 .../connection-fields/styled.ts => app/hook.ts}    |  18 +-
 .../register/webhook/config.ts => app/store.ts}    |  26 ++-
 .../src/{store => features}/connections/index.ts   |   4 +-
 .../config.ts => features/connections/name.tsx}    |  26 ++-
 config-ui/src/features/connections/slice.ts        | 122 +++++++++++++
 config-ui/src/features/connections/utils.ts        |  45 +++++
 .../connection-fields => features}/index.ts        |   3 +-
 config-ui/src/hooks/index.ts                       |   1 -
 config-ui/src/hooks/use-connections.ts             |  47 -----
 config-ui/src/main.tsx                             |   9 +-
 .../components/add-connection-dialog/index.tsx     |  11 +-
 .../pages/blueprint/detail/configuration-panel.tsx |  20 +--
 config-ui/src/pages/blueprint/home/index.tsx       |  37 ++--
 config-ui/src/pages/connection/detail/index.tsx    |  19 +-
 config-ui/src/pages/connection/home/index.tsx      |  74 ++++----
 config-ui/src/pages/project/home/index.tsx         |  25 +--
 .../plugins/components/connection-form/index.tsx   |  28 ++-
 .../plugins/components/connection-list/index.tsx   |  16 +-
 .../plugins/components/connection-status/index.tsx |  33 ++--
 config-ui/src/plugins/config.ts                    |   2 -
 config-ui/src/plugins/register/azure/config.tsx    |   2 -
 config-ui/src/plugins/register/bamboo/config.ts    |   2 -
 .../src/plugins/register/bitbucket/config.tsx      |   2 -
 config-ui/src/plugins/register/github/config.tsx   |   2 -
 config-ui/src/plugins/register/gitlab/config.tsx   |   2 -
 config-ui/src/plugins/register/jenkins/config.ts   |   2 -
 config-ui/src/plugins/register/jira/config.tsx     |   2 -
 .../src/plugins/register/pagerduty/config.tsx      |   2 -
 config-ui/src/plugins/register/sonarqube/config.ts |   2 -
 config-ui/src/plugins/register/tapd/config.tsx     |   2 -
 .../plugins/register/teambition/assets/icon.svg    |   5 -
 .../src/plugins/register/teambition/config.tsx     |  97 ----------
 .../teambition/connection-fields/tenant-id.tsx     |  72 --------
 .../teambition/connection-fields/tenant-type.tsx   |  76 --------
 .../register/webhook/components/create-dialog.tsx  |   4 -
 .../register/webhook/components/delete-dialog.tsx  |   4 -
 .../register/webhook/components/edit-dialog.tsx    |   4 -
 config-ui/src/plugins/register/webhook/config.ts   |   2 -
 .../src/plugins/register/webhook/connection.tsx    |   5 +-
 config-ui/src/plugins/register/zentao/config.tsx   |   2 -
 config-ui/src/plugins/types.ts                     |   6 -
 config-ui/src/plugins/utils.ts                     |   3 +-
 config-ui/src/routes/layout/layout.tsx             | 198 +++++++++++----------
 config-ui/src/store/connections/context.tsx        | 182 -------------------
 config-ui/src/store/index.ts                       |   1 -
 .../connections/types.ts => types/connection.ts}   |  16 +-
 .../register/teambition => types}/index.ts         |   2 +-
 config-ui/yarn.lock                                | 133 +++++++++++++-
 50 files changed, 580 insertions(+), 823 deletions(-)

diff --git a/config-ui/package.json b/config-ui/package.json
index e0e49a479..fb4cf6220 100644
--- a/config-ui/package.json
+++ b/config-ui/package.json
@@ -27,6 +27,7 @@
     "@blueprintjs/datetime2": "^1.0.10",
     "@blueprintjs/popover2": "^2.0.10",
     "@blueprintjs/select": "^5.0.10",
+    "@reduxjs/toolkit": "^1.9.7",
     "ahooks": "^3.7.8",
     "axios": "^0.21.4",
     "classnames": "^2.3.2",
@@ -39,8 +40,10 @@
     "react-copy-to-clipboard": "^5.1.0",
     "react-dom": "17.0.2",
     "react-is": "^18.2.0",
+    "react-redux": "^8.1.3",
     "react-router-dom": "^6.14.1",
     "react-transition-group": "^4.4.5",
+    "redux": "^4.2.1",
     "styled-components": "^5.3.6"
   },
   "devDependencies": {
diff --git a/config-ui/src/api/connection/types.ts 
b/config-ui/src/api/connection/types.ts
index a61f954a7..2d83c0c2b 100644
--- a/config-ui/src/api/connection/types.ts
+++ b/config-ui/src/api/connection/types.ts
@@ -27,6 +27,8 @@ export type Connection = {
   proxy: string;
   apiKey?: string;
   dbUrl?: string;
+  appId?: string;
+  secretKey?: string;
 };
 
 export type ConnectionForm = {
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/styled.ts 
b/config-ui/src/app/hook.ts
similarity index 72%
rename from 
config-ui/src/plugins/register/teambition/connection-fields/styled.ts
rename to config-ui/src/app/hook.ts
index 11e47a3d0..c945a1986 100644
--- a/config-ui/src/plugins/register/teambition/connection-fields/styled.ts
+++ b/config-ui/src/app/hook.ts
@@ -16,17 +16,9 @@
  *
  */
 
-import styled from 'styled-components';
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
 
-export const Label = styled.label`
-  font-size: 16px;
-  font-weight: 600;
-`;
-
-export const LabelInfo = styled.i`
-  color: #ff8b8b;
-`;
-
-export const LabelDescription = styled.p`
-  margin: 0;
-`;
+type DispatchFunc = () => AppDispatch;
+export const useAppDispatch: DispatchFunc = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
diff --git a/config-ui/src/plugins/register/webhook/config.ts 
b/config-ui/src/app/store.ts
similarity index 65%
copy from config-ui/src/plugins/register/webhook/config.ts
copy to config-ui/src/app/store.ts
index 25088545d..4e27d7f1d 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/app/store.ts
@@ -16,21 +16,15 @@
  *
  */
 
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
+import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
+import ConnectionSlice from '@/features/connections/slice';
 
-import Icon from './assets/icon.svg';
-
-export const WebhookConfig: PluginConfigType = {
-  plugin: 'webhook',
-  name: 'Webhook',
-  type: PluginType.Connection,
-  icon: Icon,
-  sort: 100,
-  connection: {
-    docLink: '',
-    fields: [],
-    initialValues: {},
+export const store = configureStore({
+  reducer: {
+    connections: ConnectionSlice,
   },
-  dataScope: {},
-};
+});
+
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType<typeof store.getState>;
+export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, 
unknown, Action<string>>;
diff --git a/config-ui/src/store/connections/index.ts 
b/config-ui/src/features/connections/index.ts
similarity index 93%
rename from config-ui/src/store/connections/index.ts
rename to config-ui/src/features/connections/index.ts
index 34ae5c1d0..51ef44992 100644
--- a/config-ui/src/store/connections/index.ts
+++ b/config-ui/src/features/connections/index.ts
@@ -16,5 +16,5 @@
  *
  */
 
-export * from './types';
-export * from './context';
+export * from './slice';
+export * from './name';
diff --git a/config-ui/src/plugins/register/webhook/config.ts 
b/config-ui/src/features/connections/name.tsx
similarity index 66%
copy from config-ui/src/plugins/register/webhook/config.ts
copy to config-ui/src/features/connections/name.tsx
index 25088545d..51482969d 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/features/connections/name.tsx
@@ -16,21 +16,17 @@
  *
  */
 
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
+import { useAppSelector } from '@/app/hook';
 
-import Icon from './assets/icon.svg';
+import { selectConnection } from './slice';
+import { IConnection } from '@/types';
 
-export const WebhookConfig: PluginConfigType = {
-  plugin: 'webhook',
-  name: 'Webhook',
-  type: PluginType.Connection,
-  icon: Icon,
-  sort: 100,
-  connection: {
-    docLink: '',
-    fields: [],
-    initialValues: {},
-  },
-  dataScope: {},
+interface Props {
+  plugin: string;
+  connectionId: ID;
+}
+
+export const ConnectionName = ({ plugin, connectionId }: Props) => {
+  const connection = useAppSelector((state) => selectConnection(state, 
`${plugin}-${connectionId}`)) as IConnection;
+  return <span>{connection.name}</span>;
 };
diff --git a/config-ui/src/features/connections/slice.ts 
b/config-ui/src/features/connections/slice.ts
new file mode 100644
index 000000000..66b534d1f
--- /dev/null
+++ b/config-ui/src/features/connections/slice.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { flatten } from 'lodash';
+
+import API from '@/api';
+import type { ConnectionForm } from '@/api/connection/types';
+import { RootState } from '@/app/store';
+import { PluginConfig } from '@/plugins';
+import { IConnection, IConnectionStatus } from '@/types';
+
+import { transformConnection } from './utils';
+
+const initialState: {
+  connections: IConnection[];
+} = {
+  connections: [],
+};
+
+export const init = createAsyncThunk('connections/init', async () => {
+  const res = await Promise.all(
+    PluginConfig.map(async ({ plugin }) => {
+      const connections = await API.connection.list(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);
+  return {
+    plugin,
+    connections: connections.map((connection) => transformConnection(plugin, 
connection)),
+  };
+});
+
+export const testConnection = createAsyncThunk(
+  'connections/testConnection',
+  async ({ unique, plugin, endpoint, proxy, token, username, password, 
authMethod, secretKey, appId }: IConnection) => {
+    const res = await API.connection.test(plugin, {
+      endpoint,
+      proxy,
+      token,
+      username,
+      password,
+      authMethod,
+      secretKey,
+      appId,
+    });
+
+    return {
+      unique,
+      status: res.success ? IConnectionStatus.ONLINE : 
IConnectionStatus.OFFLINE,
+    };
+  },
+);
+
+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 (payload: 
ConnectionForm) => {});
+
+export const slice = createSlice({
+  name: 'connections',
+  initialState,
+  reducers: {},
+  extraReducers(builder) {
+    builder
+      .addCase(init.fulfilled, (state, action) => {
+        state.connections = action.payload;
+      })
+      .addCase(fetchConnections.fulfilled, (state, action) => {
+        state.connections = 
state.connections.concat(action.payload.connections);
+      })
+      .addCase(addConnection.fulfilled, (state, action) => {
+        state.connections.push(action.payload);
+      })
+      .addCase(testConnection.pending, (state, action) => {
+        const existingConnection = state.connections.find((cs) => cs.unique 
=== action.meta.arg.unique);
+        if (existingConnection) {
+          existingConnection.status = IConnectionStatus.TESTING;
+        }
+      })
+      .addCase(testConnection.fulfilled, (state, action) => {
+        const existingConnection = state.connections.find((cs) => cs.unique 
=== action.payload.unique);
+        if (existingConnection) {
+          existingConnection.status = action.payload.status;
+        }
+      });
+  },
+});
+
+export const {} = slice.actions;
+
+export default slice.reducer;
+
+export const selectAllConnections = (state: RootState) => 
state.connections.connections;
+
+export const selectConnections = (state: RootState, plugin: string) =>
+  state.connections.connections.filter((connection) => connection.plugin === 
plugin);
+
+export const selectConnection = (state: RootState, unique: string) =>
+  state.connections.connections.find((cs) => cs.unique === unique);
diff --git a/config-ui/src/features/connections/utils.ts 
b/config-ui/src/features/connections/utils.ts
new file mode 100644
index 000000000..00161a4ca
--- /dev/null
+++ b/config-ui/src/features/connections/utils.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 T from '@/api/connection/types';
+import type { PluginConfigType } from '@/plugins';
+import { PluginConfig } from '@/plugins';
+
+import { IConnection, IConnectionStatus } from '@/types';
+
+export const transformConnection = (plugin: string, connection: T.Connection): 
IConnection => {
+  const config = PluginConfig.find((p) => p.plugin === plugin) as 
PluginConfigType;
+  return {
+    unique: `${plugin}-${connection.id}`,
+    plugin,
+    pluginName: config.name,
+    id: connection.id,
+    name: connection.name,
+    status: IConnectionStatus.IDLE,
+    icon: config.icon,
+    isBeta: config.isBeta ?? false,
+    endpoint: connection.endpoint,
+    proxy: connection.proxy,
+    authMethod: connection.authMethod,
+    token: connection.token,
+    username: connection.username,
+    password: connection.password,
+    appId: connection.appId,
+    secretKey: connection.secretKey,
+  };
+};
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/index.ts 
b/config-ui/src/features/index.ts
similarity index 93%
rename from config-ui/src/plugins/register/teambition/connection-fields/index.ts
rename to config-ui/src/features/index.ts
index b2481eb94..4e6e8a4de 100644
--- a/config-ui/src/plugins/register/teambition/connection-fields/index.ts
+++ b/config-ui/src/features/index.ts
@@ -16,5 +16,4 @@
  *
  */
 
-export * from './tenant-id';
-export * from './tenant-type';
+export * from './connections';
diff --git a/config-ui/src/hooks/index.ts b/config-ui/src/hooks/index.ts
index cba922842..7f38b5d6f 100644
--- a/config-ui/src/hooks/index.ts
+++ b/config-ui/src/hooks/index.ts
@@ -17,7 +17,6 @@
  */
 
 export * from './use-auto-refresh';
-export * from './use-connections';
 export * from './use-refresh-data';
 export * from './use-tips';
 export * from './user-proxy-prefix';
diff --git a/config-ui/src/hooks/use-connections.ts 
b/config-ui/src/hooks/use-connections.ts
deleted file mode 100644
index f129c13ee..000000000
--- a/config-ui/src/hooks/use-connections.ts
+++ /dev/null
@@ -1,47 +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 { useContext } from 'react';
-
-import { ConnectionContext } from '@/store';
-
-type UseConnectionsProps = {
-  unique?: string;
-  plugin?: string;
-  filter?: string[];
-  filterBeta?: boolean;
-  filterPlugin?: string[];
-};
-
-export const useConnections = (props?: UseConnectionsProps) => {
-  const { unique, plugin, filter, filterBeta, filterPlugin } = props || {};
-
-  const { connections, onGet, onTest, onRefresh } = 
useContext(ConnectionContext);
-
-  return {
-    connection: unique ? connections.find((cs) => cs.unique === unique) : null,
-    connections: connections
-      .filter((cs) => (plugin ? cs.plugin === plugin : true))
-      .filter((cs) => (filter ? !filter.includes(cs.unique) : true))
-      .filter((cs) => (filterBeta ? !cs.isBeta : true))
-      .filter((cs) => (filterPlugin ? !filterPlugin.includes(cs.plugin) : 
true)),
-    onGet,
-    onTest,
-    onRefresh,
-  };
-};
diff --git a/config-ui/src/main.tsx b/config-ui/src/main.tsx
index 682db7df5..155d47978 100644
--- a/config-ui/src/main.tsx
+++ b/config-ui/src/main.tsx
@@ -17,8 +17,15 @@
  */
 
 import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
 
 import { App } from './App';
+import { store } from './app/store';
 import './index.css';
 
-ReactDOM.render(<App />, document.getElementById('root'));
+ReactDOM.render(
+  <Provider store={store}>
+    <App />
+  </Provider>,
+  document.getElementById('root'),
+);
diff --git 
a/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx
 
b/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx
index e648a0bad..6ef875322 100644
--- 
a/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx
+++ 
b/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx
@@ -19,10 +19,11 @@
 import { useState, useMemo } from 'react';
 import { Button, Intent } from '@blueprintjs/core';
 
+import { useAppSelector } from '@/app/hook';
 import { Dialog, FormItem, Selector, Buttons } from '@/components';
-import { useConnections } from '@/hooks';
-import { DataScopeSelect, getPluginScopeId } from '@/plugins';
-import type { ConnectionItemType } from '@/store';
+import { selectAllConnections } from '@/features';
+import { DataScopeSelect } from '@/plugins';
+import { IConnection } from '@/types';
 
 interface Props {
   disabled: string[];
@@ -32,9 +33,9 @@ interface Props {
 
 export const AddConnectionDialog = ({ disabled = [], onCancel, onSubmit }: 
Props) => {
   const [step, setStep] = useState(1);
-  const [selectedConnection, setSelectedConnection] = 
useState<ConnectionItemType>();
+  const [selectedConnection, setSelectedConnection] = useState<IConnection>();
 
-  const { connections } = useConnections({ filterPlugin: ['webhook'] });
+  const connections = useAppSelector(selectAllConnections);
 
   const disabledItems = useMemo(
     () => connections.filter((cs) => (disabled.length ? 
disabled.includes(cs.unique) : false)),
diff --git a/config-ui/src/pages/blueprint/detail/configuration-panel.tsx 
b/config-ui/src/pages/blueprint/detail/configuration-panel.tsx
index 4ab0c135d..9890aea04 100644
--- a/config-ui/src/pages/blueprint/detail/configuration-panel.tsx
+++ b/config-ui/src/pages/blueprint/detail/configuration-panel.tsx
@@ -23,7 +23,7 @@ import { Button, Intent } from '@blueprintjs/core';
 import API from '@/api';
 import { IconButton, Table, NoData, Buttons } from '@/components';
 import { getCron } from '@/config';
-import { useConnections } from '@/hooks';
+import { ConnectionName } from '@/features';
 import { getPluginConfig } from '@/plugins';
 import { formatTime, operator } from '@/utils';
 
@@ -52,20 +52,16 @@ export const ConfigurationPanel = ({ from, blueprint, 
onRefresh, onChangeTab }:
     setRawPlan(JSON.stringify(blueprint.plan, null, '  '));
   }, [blueprint]);
 
-  const { onGet } = useConnections();
-
   const connections = useMemo(
     () =>
       blueprint.connections
         .filter((cs) => cs.pluginName !== 'webhook')
         .map((cs: any) => {
-          const unique = `${cs.pluginName}-${cs.connectionId}`;
           const plugin = getPluginConfig(cs.pluginName);
-          const connection = onGet(unique);
           return {
-            unique,
+            plugin: plugin.plugin,
+            connectionId: cs.connectionId,
             icon: plugin.icon,
-            name: connection?.name,
             scope: cs.scopes,
           };
         })
@@ -205,10 +201,10 @@ export const ConfigurationPanel = ({ from, blueprint, 
onRefresh, onChangeTab }:
               </Buttons>
               <S.ConnectionList>
                 {connections.map((cs) => (
-                  <S.ConnectionItem key={cs.unique}>
+                  <S.ConnectionItem key={`${cs.plugin}-${cs.connectionId}`}>
                     <div className="title">
                       <img src={cs.icon} alt="" />
-                      <span>{cs.name}</span>
+                      <ConnectionName plugin={cs.plugin} 
connectionId={cs.connectionId} />
                     </div>
                     <div className="count">
                       <span>{cs.scope.length} data scope</span>
@@ -217,8 +213,8 @@ export const ConfigurationPanel = ({ from, blueprint, 
onRefresh, onChangeTab }:
                       <Link
                         to={
                           from === FromEnum.blueprint
-                            ? `/blueprints/${blueprint.id}/${cs.unique}`
-                            : 
`/projects/${encodeName(blueprint.projectName)}/${cs.unique}`
+                            ? 
`/blueprints/${blueprint.id}/${cs.plugin}-${cs.connectionId}`
+                            : 
`/projects/${encodeName(blueprint.projectName)}/${cs.plugin}-${cs.connectionId}`
                         }
                       >
                         Edit Data Scope and Scope Config
@@ -273,7 +269,7 @@ export const ConfigurationPanel = ({ from, blueprint, 
onRefresh, onChangeTab }:
       )}
       {type === 'add-connection' && (
         <AddConnectionDialog
-          disabled={connections.map((cs) => cs.unique)}
+          disabled={connections.map((cs) => `${cs.plugin}-${cs.connectionId}`)}
           onCancel={handleCancel}
           onSubmit={(connection) =>
             handleUpdate({
diff --git a/config-ui/src/pages/blueprint/home/index.tsx 
b/config-ui/src/pages/blueprint/home/index.tsx
index 96946ebee..98c0d7d49 100644
--- a/config-ui/src/pages/blueprint/home/index.tsx
+++ b/config-ui/src/pages/blueprint/home/index.tsx
@@ -24,10 +24,11 @@ import dayjs from 'dayjs';
 import API from '@/api';
 import { PageHeader, Table, IconButton, TextTooltip, Dialog } from 
'@/components';
 import { getCronOptions, cronPresets, getCron } from '@/config';
-import { useConnections, useRefreshData } from '@/hooks';
+import { ConnectionName } from '@/features';
+import { useRefreshData } from '@/hooks';
 import { formatTime, operator } from '@/utils';
 
-import { ModeEnum } from '../types';
+import { ModeEnum, BlueprintType } from '../types';
 
 import * as S from './styled';
 
@@ -41,29 +42,13 @@ export const BlueprintHomePage = () => {
   const [mode, setMode] = useState(ModeEnum.normal);
   const [saving, setSaving] = useState(false);
 
-  const { onGet } = useConnections();
   const { ready, data } = useRefreshData(
     () => API.blueprint.list({ type: type.toLocaleUpperCase(), page, pageSize 
}),
     [version, type, page, pageSize],
   );
 
   const [options, presets] = useMemo(() => [getCronOptions(), 
cronPresets.map((preset) => preset.config)], []);
-  const [dataSource, total] = useMemo(
-    () => [
-      (data?.blueprints ?? []).map((it) => {
-        const connections =
-          it.connections
-            .filter((cs) => cs.pluginName !== 'webhook')
-            .map((cs) => onGet(`${cs.pluginName}-${cs.connectionId}`) || 
`${cs.pluginName}-${cs.connectionId}`) ?? [];
-        return {
-          ...it,
-          connections: connections.map((cs) => cs.name),
-        };
-      }),
-      data?.count ?? 0,
-    ],
-    [data],
-  );
+  const [dataSource, total] = useMemo(() => [data?.blueprints ?? [], 
data?.count ?? 0], [data]);
 
   const handleShowDialog = () => setIsOpen(true);
   const handleHideDialog = () => {
@@ -144,11 +129,21 @@ export const BlueprintHomePage = () => {
               dataIndex: ['mode', 'connections'],
               key: 'connections',
               align: 'center',
-              render: ({ mode, connections }) => {
+              render: ({ mode, connections }: Pick<BlueprintType, 'mode' | 
'connections'>) => {
                 if (mode === ModeEnum.advanced) {
                   return 'Advanced Mode';
                 }
-                return connections.join(',');
+                return (
+                  <>
+                    {connections.map((it) => (
+                      <ConnectionName
+                        key={`${it.pluginName}-${it.connectionId}`}
+                        plugin={it.pluginName}
+                        connectionId={it.connectionId}
+                      />
+                    ))}
+                  </>
+                );
               },
             },
             {
diff --git a/config-ui/src/pages/connection/detail/index.tsx 
b/config-ui/src/pages/connection/detail/index.tsx
index 908b403ac..2668392f3 100644
--- a/config-ui/src/pages/connection/detail/index.tsx
+++ b/config-ui/src/pages/connection/detail/index.tsx
@@ -16,13 +16,15 @@
  *
  */
 
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useMemo } from 'react';
 import { useParams, useNavigate, Link } from 'react-router-dom';
 import { Button, Intent } from '@blueprintjs/core';
 
 import API from '@/api';
+import { useAppSelector } from '@/app/hook';
 import { PageHeader, Buttons, Dialog, IconButton, Table, Message, toast } from 
'@/components';
-import { useTips, useConnections, useRefreshData } from '@/hooks';
+import { selectConnection } from '@/features';
+import { useTips, useRefreshData } from '@/hooks';
 import ClearImg from '@/images/icons/clear.svg';
 import {
   ConnectionForm,
@@ -33,6 +35,7 @@ import {
   ScopeConfigForm,
   ScopeConfigSelect,
 } from '@/plugins';
+import { IConnection } from '@/types';
 import { operator } from '@/utils';
 
 import * as S from './styled';
@@ -68,15 +71,15 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) 
=> {
   const [conflict, setConflict] = useState<string[]>([]);
   const [errorMsg, setErrorMsg] = useState('');
 
+  const connection = useAppSelector((state) => selectConnection(state, 
`${plugin}-${connectionId}`)) as IConnection;
   const navigate = useNavigate();
-  const { onGet, onTest, onRefresh } = useConnections();
   const { setTips } = useTips();
   const { ready, data } = useRefreshData(
     () => API.scope.list(plugin, connectionId, { page, pageSize, blueprint: 
true }),
     [version, page, pageSize],
   );
 
-  const { unique, status, name, icon } = onGet(`${plugin}-${connectionId}`) || 
{};
+  const { name, icon } = connection;
 
   const pluginConfig = useMemo(() => getPluginConfig(plugin), [plugin]);
 
@@ -94,10 +97,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) 
=> {
     [data],
   );
 
-  useEffect(() => {
-    onTest(`${plugin}-${connectionId}`);
-  }, [plugin, connectionId]);
-
   const handleHideDialog = () => {
     setType(undefined);
   };
@@ -129,7 +128,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) 
=> {
 
     if (res.status === 'success') {
       toast.success('Delete Connection Successful.');
-      onRefresh(plugin);
       navigate('/connections');
     } else if (res.status === 'conflict') {
       setType('deleteConnectionFailed');
@@ -146,7 +144,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) 
=> {
   };
 
   const handleUpdate = () => {
-    onRefresh(plugin);
     handleHideDialog();
   };
 
@@ -250,7 +247,7 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) 
=> {
       extra={
         <S.PageHeaderExtra>
           <span style={{ marginRight: 4 }}>Status:</span>
-          <ConnectionStatus status={status} unique={unique} onTest={onTest} />
+          <ConnectionStatus connection={connection} />
           <Buttons style={{ marginLeft: 8 }}>
             <Button outlined intent={Intent.PRIMARY} icon="annotation" 
text="Edit" onClick={handleShowUpdateDialog} />
             <Button intent={Intent.DANGER} icon="trash" text="Delete" 
onClick={handleShowDeleteDialog} />
diff --git a/config-ui/src/pages/connection/home/index.tsx 
b/config-ui/src/pages/connection/home/index.tsx
index d0dcccbcc..5df3a6ad6 100644
--- a/config-ui/src/pages/connection/home/index.tsx
+++ b/config-ui/src/pages/connection/home/index.tsx
@@ -20,10 +20,10 @@ import { useState, useMemo } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { Tag, Intent } from '@blueprintjs/core';
 
+import { useAppSelector } from '@/app/hook';
 import { Dialog } from '@/components';
-import { useConnections } from '@/hooks';
-import type { PluginConfigType } from '@/plugins';
-import { PluginConfig, PluginType, ConnectionList, ConnectionForm } from 
'@/plugins';
+import { selectAllConnections } from '@/features/connections';
+import { PluginConfig, PluginConfigType, ConnectionList, ConnectionForm } from 
'@/plugins';
 
 import * as S from './styled';
 
@@ -31,26 +31,22 @@ export const ConnectionHomePage = () => {
   const [type, setType] = useState<'list' | 'form'>();
   const [pluginConfig, setPluginConfig] = useState<PluginConfigType>();
 
-  const { connections, onRefresh } = useConnections();
+  const connections = useAppSelector(selectAllConnections);
+
   const navigate = useNavigate();
 
-  const [plugins, webhook] = useMemo(
-    () => [
-      PluginConfig.filter((p) => p.type === PluginType.Connection && p.plugin 
!== 'webhook').map((p) => ({
+  const plugins = useMemo(
+    () =>
+      PluginConfig.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,
-      },
-    ],
     [connections],
   );
 
-  const handleShowListDialog = (config: PluginConfigType) => {
+  const handleShowListDialog = (pluginConfig: PluginConfigType) => {
     setType('list');
-    setPluginConfig(config);
+    setPluginConfig(pluginConfig);
   };
 
   const handleShowFormDialog = () => {
@@ -62,8 +58,7 @@ export const ConnectionHomePage = () => {
     setPluginConfig(undefined);
   };
 
-  const handleCreateSuccess = async (plugin: string, id: ID) => {
-    onRefresh(plugin);
+  const handleSuccessAfter = async (plugin: string, id: ID) => {
     navigate(`/connections/${plugin}/${id}`);
   };
 
@@ -82,18 +77,20 @@ export const ConnectionHomePage = () => {
           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>
-          ))}
+          {plugins
+            .filter((p) => p.plugin !== 'webhook')
+            .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">
@@ -103,11 +100,20 @@ export const ConnectionHomePage = () => {
           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>
+          {plugins
+            .filter((p) => p.plugin === 'webhook')
+            .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>
       {type === 'list' && pluginConfig && (
@@ -141,7 +147,7 @@ export const ConnectionHomePage = () => {
         >
           <ConnectionForm
             plugin={pluginConfig.plugin}
-            onSuccess={(id) => handleCreateSuccess(pluginConfig.plugin, id)}
+            onSuccess={(id) => handleSuccessAfter(pluginConfig.plugin, id)}
           />
         </Dialog>
       )}
diff --git a/config-ui/src/pages/project/home/index.tsx 
b/config-ui/src/pages/project/home/index.tsx
index 54b167d44..ef79ce889 100644
--- a/config-ui/src/pages/project/home/index.tsx
+++ b/config-ui/src/pages/project/home/index.tsx
@@ -24,7 +24,8 @@ import dayjs from 'dayjs';
 import API from '@/api';
 import { PageHeader, Table, Dialog, ExternalLink, IconButton, toast } from 
'@/components';
 import { getCron, cronPresets } from '@/config';
-import { useConnections, useRefreshData } from '@/hooks';
+import { ConnectionName } from '@/features';
+import { useRefreshData } from '@/hooks';
 import { DOC_URL } from '@/release';
 import { formatTime, operator } from '@/utils';
 import { PipelineStatus } from '@/routes/pipeline';
@@ -44,7 +45,6 @@ export const ProjectHomePage = () => {
   const [saving, setSaving] = useState(false);
 
   const { ready, data } = useRefreshData(() => API.project.list({ page, 
pageSize }), [version, page, pageSize]);
-  const { onGet } = useConnections();
 
   const navigate = useNavigate();
 
@@ -139,14 +139,19 @@ export const ProjectHomePage = () => {
             dataIndex: 'connections',
             key: 'connections',
             render: (val: BlueprintType['connections']) =>
-              !val || !val.length
-                ? 'N/A'
-                : val
-                    .map((it) => {
-                      const cs = onGet(`${it.pluginName}-${it.connectionId}`);
-                      return cs?.name;
-                    })
-                    .join(', '),
+              !val || !val.length ? (
+                'N/A'
+              ) : (
+                <>
+                  {val.map((it) => (
+                    <ConnectionName
+                      key={`${it.pluginName}-${it.connectionId}`}
+                      plugin={it.pluginName}
+                      connectionId={it.connectionId}
+                    />
+                  ))}
+                </>
+              ),
           },
           {
             title: 'Sync Frequency',
diff --git a/config-ui/src/plugins/components/connection-form/index.tsx 
b/config-ui/src/plugins/components/connection-form/index.tsx
index 4ef0a9573..cb7ffa549 100644
--- a/config-ui/src/plugins/components/connection-form/index.tsx
+++ b/config-ui/src/plugins/components/connection-form/index.tsx
@@ -21,9 +21,10 @@ import { Button, Intent } from '@blueprintjs/core';
 import { pick } from 'lodash';
 
 import API from '@/api';
-import { ExternalLink, PageLoading, Buttons } from '@/components';
-import { useRefreshData } from '@/hooks';
-import { getPluginConfig } from '@/plugins';
+import { useAppDispatch, useAppSelector } from '@/app/hook';
+import { ExternalLink, Buttons } from '@/components';
+import { selectConnection } from '@/features/connections';
+import { PluginConfig, PluginConfigType } from '@/plugins';
 import { operator } from '@/utils';
 
 import { Form } from './fields';
@@ -40,23 +41,18 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
   const [errors, setErrors] = useState<Record<string, any>>({});
   const [operating, setOperating] = useState(false);
 
+  const dispatch = useAppDispatch();
+  const connection = useAppSelector((state) => selectConnection(state, 
`${plugin}-${connectionId}`));
+
   const {
     name,
     connection: { docLink, fields, initialValues },
-  } = useMemo(() => getPluginConfig(plugin), [plugin]);
+  } = useMemo(() => PluginConfig.find((p) => p.plugin === plugin) as 
PluginConfigType, [plugin]);
 
   const disabled = useMemo(() => {
     return Object.values(errors).some((value) => value);
   }, [errors]);
 
-  const { ready, data } = useRefreshData(async () => {
-    if (!connectionId) {
-      return {};
-    }
-
-    return API.connection.get(plugin, connectionId);
-  }, [plugin, connectionId]);
-
   const handleTest = async () => {
     await operator(
       () =>
@@ -73,7 +69,7 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
             'secretKey',
             'tenantId',
             'tenantType',
-            "dbUrl",
+            'dbUrl',
           ]),
         ),
       {
@@ -98,10 +94,6 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
     }
   };
 
-  if (connectionId && !ready) {
-    return <PageLoading />;
-  }
-
   return (
     <S.Wrapper>
       <S.Tips>
@@ -112,7 +104,7 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
         <Form
           name={name}
           fields={fields}
-          initialValues={{ ...initialValues, ...data }}
+          initialValues={{ ...initialValues, ...(connection ?? {}) }}
           values={values}
           errors={errors}
           setValues={setValues}
diff --git a/config-ui/src/plugins/components/connection-list/index.tsx 
b/config-ui/src/plugins/components/connection-list/index.tsx
index 634dd7dad..d113c1788 100644
--- a/config-ui/src/plugins/components/connection-list/index.tsx
+++ b/config-ui/src/plugins/components/connection-list/index.tsx
@@ -19,8 +19,9 @@
 import { Link } from 'react-router-dom';
 import { Button, Intent } from '@blueprintjs/core';
 
+import { useAppSelector } from '@/app/hook';
 import { Table } from '@/components';
-import { useConnections } from '@/hooks';
+import { selectConnections } from '@/features/connections';
 import { ConnectionStatus } from '@/plugins';
 
 import { WebHookConnection } from '@/plugins/register/webhook';
@@ -31,16 +32,12 @@ interface Props {
 }
 
 export const ConnectionList = ({ plugin, onCreate }: Props) => {
+  const connections = useAppSelector((state) => selectConnections(state, 
plugin));
+
   if (plugin === 'webhook') {
     return <WebHookConnection />;
   }
 
-  return <BaseList plugin={plugin} onCreate={onCreate} />;
-};
-
-const BaseList = ({ plugin, onCreate }: Props) => {
-  const { connections, onTest } = useConnections();
-
   return (
     <>
       <Table
@@ -53,10 +50,9 @@ const BaseList = ({ plugin, onCreate }: Props) => {
           },
           {
             title: 'Status',
-            dataIndex: ['status', 'unique'],
             key: 'status',
             width: 150,
-            render: ({ status, unique }) => <ConnectionStatus status={status} 
unique={unique} onTest={onTest} />,
+            render: (_, row) => <ConnectionStatus connection={row} />,
           },
           {
             title: '',
@@ -66,7 +62,7 @@ const BaseList = ({ plugin, onCreate }: Props) => {
             render: ({ plugin, id }) => <Link 
to={`/connections/${plugin}/${id}`}>Details</Link>,
           },
         ]}
-        dataSource={connections.filter((cs) => cs.plugin === plugin)}
+        dataSource={connections}
         noData={{
           text: 'There is no data connection yet. Please add a new 
connection.',
         }}
diff --git a/config-ui/src/plugins/components/connection-status/index.tsx 
b/config-ui/src/plugins/components/connection-status/index.tsx
index 3475194d4..136f9b59b 100644
--- a/config-ui/src/plugins/components/connection-status/index.tsx
+++ b/config-ui/src/plugins/components/connection-status/index.tsx
@@ -18,8 +18,10 @@
 
 import styled from 'styled-components';
 
+import { useAppDispatch } from '@/app/hook';
 import { IconButton } from '@/components';
-import { ConnectionStatusEnum } from '@/store';
+import { testConnection } from '@/features/connections';
+import { IConnection, IConnectionStatus } from '@/types';
 
 const Wrapper = styled.div`
   display: inline-flex;
@@ -35,29 +37,28 @@ const Wrapper = styled.div`
 `;
 
 const STATUS_MAP = {
-  [`${ConnectionStatusEnum.NULL}`]: 'Test',
-  [`${ConnectionStatusEnum.TESTING}`]: 'Testing',
-  [`${ConnectionStatusEnum.ONLINE}`]: 'Connected',
-  [`${ConnectionStatusEnum.OFFLINE}`]: 'Disconnected',
+  [`${IConnectionStatus.IDLE}`]: 'Test',
+  [`${IConnectionStatus.TESTING}`]: 'Testing',
+  [`${IConnectionStatus.ONLINE}`]: 'Connected',
+  [`${IConnectionStatus.OFFLINE}`]: 'Disconnected',
 };
 
 interface Props {
-  status: ConnectionStatusEnum;
-  unique: string;
-  onTest: (unique: string) => void;
+  connection: IConnection;
 }
 
-export const ConnectionStatus = ({ status, unique, onTest }: Props) => {
+export const ConnectionStatus = ({ connection }: Props) => {
+  const { status } = connection;
+
+  const dispatch = useAppDispatch();
+
+  const handleTest = () => dispatch(testConnection(connection));
+
   return (
     <Wrapper>
       <span className={status}>{STATUS_MAP[status]}</span>
-      {status !== ConnectionStatusEnum.ONLINE && (
-        <IconButton
-          loading={status === ConnectionStatusEnum.TESTING}
-          icon="repeat"
-          tooltip="Retry"
-          onClick={() => onTest(unique)}
-        />
+      {status !== IConnectionStatus.ONLINE && (
+        <IconButton loading={status === IConnectionStatus.TESTING} 
icon="repeat" tooltip="Retry" onClick={handleTest} />
       )}
     </Wrapper>
   );
diff --git a/config-ui/src/plugins/config.ts b/config-ui/src/plugins/config.ts
index ebac6b5fe..d840f84a8 100644
--- a/config-ui/src/plugins/config.ts
+++ b/config-ui/src/plugins/config.ts
@@ -28,7 +28,6 @@ import { PagerDutyConfig } from './register/pagerduty';
 import { SonarQubeConfig } from './register/sonarqube';
 import { TAPDConfig } from './register/tapd';
 import { WebhookConfig } from './register/webhook';
-import { TeambitionConfig } from './register/teambition';
 import { ZenTaoConfig } from './register/zentao';
 
 export const PluginConfig: PluginConfigType[] = [
@@ -42,7 +41,6 @@ export const PluginConfig: PluginConfigType[] = [
   PagerDutyConfig,
   SonarQubeConfig,
   TAPDConfig,
-  TeambitionConfig,
   ZenTaoConfig,
   WebhookConfig,
 ].sort((a, b) => a.sort - b.sort);
diff --git a/config-ui/src/plugins/register/azure/config.tsx 
b/config-ui/src/plugins/register/azure/config.tsx
index 500a4e389..799d15a3e 100644
--- a/config-ui/src/plugins/register/azure/config.tsx
+++ b/config-ui/src/plugins/register/azure/config.tsx
@@ -20,13 +20,11 @@ import { ExternalLink } from '@/components';
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { BaseURL } from './connection-fields';
 
 export const AzureConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'azuredevops',
   name: 'Azure DevOps',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/bamboo/config.ts 
b/config-ui/src/plugins/register/bamboo/config.ts
index 10ccbf44b..b2c78850c 100644
--- a/config-ui/src/plugins/register/bamboo/config.ts
+++ b/config-ui/src/plugins/register/bamboo/config.ts
@@ -19,12 +19,10 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const BambooConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'bamboo',
   name: 'Bamboo',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/bitbucket/config.tsx 
b/config-ui/src/plugins/register/bitbucket/config.tsx
index 11c03bb3d..97840f382 100644
--- a/config-ui/src/plugins/register/bitbucket/config.tsx
+++ b/config-ui/src/plugins/register/bitbucket/config.tsx
@@ -19,12 +19,10 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const BitBucketConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'bitbucket',
   name: 'BitBucket',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/github/config.tsx 
b/config-ui/src/plugins/register/github/config.tsx
index fc255675f..f79b56eaa 100644
--- a/config-ui/src/plugins/register/github/config.tsx
+++ b/config-ui/src/plugins/register/github/config.tsx
@@ -21,13 +21,11 @@ import { pick } from 'lodash';
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { Token, Graphql, GithubApp, Authentication } from 
'./connection-fields';
 
 export const GitHubConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'github',
   name: 'GitHub',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/gitlab/config.tsx 
b/config-ui/src/plugins/register/gitlab/config.tsx
index 53d11d044..74bd7f007 100644
--- a/config-ui/src/plugins/register/gitlab/config.tsx
+++ b/config-ui/src/plugins/register/gitlab/config.tsx
@@ -20,12 +20,10 @@ import { ExternalLink } from '@/components';
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const GitLabConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'gitlab',
   name: 'GitLab',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/jenkins/config.ts 
b/config-ui/src/plugins/register/jenkins/config.ts
index fc77932ac..fb08df4e7 100644
--- a/config-ui/src/plugins/register/jenkins/config.ts
+++ b/config-ui/src/plugins/register/jenkins/config.ts
@@ -19,12 +19,10 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const JenkinsConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'jenkins',
   name: 'Jenkins',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/jira/config.tsx 
b/config-ui/src/plugins/register/jira/config.tsx
index 3daa049f5..ce81ecd44 100644
--- a/config-ui/src/plugins/register/jira/config.tsx
+++ b/config-ui/src/plugins/register/jira/config.tsx
@@ -19,13 +19,11 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 import { Auth } from './connection-fields';
 
 export const JiraConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'jira',
   name: 'Jira',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/pagerduty/config.tsx 
b/config-ui/src/plugins/register/pagerduty/config.tsx
index 5c115711f..b7f7ab351 100644
--- a/config-ui/src/plugins/register/pagerduty/config.tsx
+++ b/config-ui/src/plugins/register/pagerduty/config.tsx
@@ -19,12 +19,10 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const PagerDutyConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'pagerduty',
   name: 'PagerDuty',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/sonarqube/config.ts 
b/config-ui/src/plugins/register/sonarqube/config.ts
index db6bfe17e..c64db0837 100644
--- a/config-ui/src/plugins/register/sonarqube/config.ts
+++ b/config-ui/src/plugins/register/sonarqube/config.ts
@@ -19,12 +19,10 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const SonarQubeConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'sonarqube',
   name: 'SonarQube',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/tapd/config.tsx 
b/config-ui/src/plugins/register/tapd/config.tsx
index 0625f6aab..a3a2026b3 100644
--- a/config-ui/src/plugins/register/tapd/config.tsx
+++ b/config-ui/src/plugins/register/tapd/config.tsx
@@ -20,13 +20,11 @@ import { ExternalLink } from '@/components';
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import { DataScope } from './data-scope';
 import Icon from './assets/icon.svg';
 
 export const TAPDConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'tapd',
   name: 'TAPD',
   icon: Icon,
diff --git a/config-ui/src/plugins/register/teambition/assets/icon.svg 
b/config-ui/src/plugins/register/teambition/assets/icon.svg
deleted file mode 100644
index 93d227f72..000000000
--- a/config-ui/src/plugins/register/teambition/assets/icon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg width="36" height="36" viewBox="0 0 36 36" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
-<path d="M24.8601 7.75227C24.8606 6.58118 25.3105 5.45494 26.1169 
4.60578C26.9234 3.75663 28.025 3.24932 29.1945 3.18845C26.7937 3.06965 24.1226 
3 21.345 3C10.6606 3 2 3.98733 2 5.20407C2 6.42082 10.6606 7.40814 21.345 
7.40814V12.3243H28.9487C28.7076 12.3059 28.4704 12.2535 28.244 12.1686C27.2724 
11.9079 26.414 11.3336 25.8021 10.5351C25.1902 9.73651 24.8591 8.7583 24.8601 
7.75227Z" fill="#BDCEFB"/>
-<path d="M21.345 7.40329C10.6606 7.40329 2 6.41596 2 5.19922V13.1757C2 14.0892 
6.86288 14.8717 13.7987 15.2241V30.9066L21.4024 32.3609L21.345 7.40329Z" 
fill="#7497F7"/>
-<path d="M29.428 12.3237C31.953 12.3237 34 10.2768 34 7.7517C34 5.22665 31.953 
3.17969 29.428 3.17969C26.9029 3.17969 24.856 5.22665 24.856 7.7517C24.856 
10.2768 26.9029 12.3237 29.428 12.3237Z" fill="#7497F7"/>
-</svg>
diff --git a/config-ui/src/plugins/register/teambition/config.tsx 
b/config-ui/src/plugins/register/teambition/config.tsx
deleted file mode 100644
index 079ca4d1a..000000000
--- a/config-ui/src/plugins/register/teambition/config.tsx
+++ /dev/null
@@ -1,97 +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 { DOC_URL } from '@/release';
-
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
-
-import Icon from './assets/icon.svg';
-import { ConnectionTenantId, ConnectionTenantType } from './connection-fields';
-
-export const TeambitionConfig: PluginConfigType = {
-  type: PluginType.Pipeline,
-  plugin: 'teambition',
-  name: 'Teambition',
-  isBeta: true,
-  icon: Icon,
-  sort: 100,
-  connection: {
-    docLink: DOC_URL.PLUGIN.TEAMBITION.BASIS,
-    initialValues: {
-      endpoint: 'https://open.teambition.com/api/',
-      tenantType: 'organization',
-    },
-    fields: [
-      'name',
-      {
-        key: 'endpoint',
-        subLabel: 'You do not need to enter the endpoint URL, because all 
versions use the same URL.',
-        disabled: true,
-      },
-      {
-        key: 'appId',
-        label: 'Application App Id',
-        subLabel: 'Your teambition application App Id.',
-      },
-      {
-        key: 'secretKey',
-        label: 'Application Secret Key',
-        subLabel: 'Your teambition application App Secret.',
-      },
-      ({ initialValues, values, errors, setValues, setErrors }: any) => (
-        <ConnectionTenantId
-          key="tenantId"
-          name="tenantId"
-          value={values.tenantId ?? ''}
-          error={errors.tenantId ?? ''}
-          setValue={(value) => setValues({ tenantId: value })}
-          setError={(value) => setErrors({ tenantId: value })}
-          initialValue={initialValues.tenantId}
-        />
-      ),
-      ({ initialValues, values, errors, setValues, setErrors }: any) => (
-        <ConnectionTenantType
-          key="tenantType"
-          name="tenantType"
-          value={values.tenantType ?? ''}
-          error={errors.tenantType ?? ''}
-          setValue={(value) => setValues({ tenantType: value })}
-          setError={(value) => setErrors({ tenantType: value })}
-          initialValue={initialValues.tenantType}
-        />
-      ),
-      'proxy',
-      {
-        key: 'rateLimitPerHour',
-        subLabel:
-          'By default, DevLake uses dynamic rate limit for optimized data 
collection for Teambition. But you can adjust the collection speed by entering 
a fixed value. Please note: the rate limit setting applies to all tokens you 
have entered above.',
-        learnMore: DOC_URL.PLUGIN.TEAMBITION.RATE_LIMIT,
-        externalInfo: 'Teambition does not specify a maximum value of rate 
limit.',
-        defaultValue: 5000,
-      },
-    ],
-  },
-  dataScope: {
-    title: '',
-  },
-  scopeConfig: {
-    entities: ['TICKET'],
-    transformation: {},
-  },
-};
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx 
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
deleted file mode 100644
index a5d1a64d4..000000000
--- a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
+++ /dev/null
@@ -1,72 +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.
- *
- */
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useEffect } from 'react';
-import { FormGroup, InputGroup } from '@blueprintjs/core';
-
-import * as S from './styled';
-
-interface Props {
-  name: string;
-  initialValue: string;
-  value: string;
-  error: string;
-  setValue: (value: string) => void;
-  setError: (error: string) => void;
-}
-
-export const ConnectionTenantId = ({ initialValue, value, setValue, setError 
}: Props) => {
-  useEffect(() => {
-    setValue(initialValue);
-  }, [initialValue]);
-
-  useEffect(() => {
-    setError(value ? '' : 'TenantId is required');
-  }, [value]);
-
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    setValue(e.target.value);
-  };
-
-  return (
-    <FormGroup
-      label={<S.Label>Tenant Id</S.Label>}
-      labelInfo={<S.LabelInfo>*</S.LabelInfo>}
-      subLabel={<S.LabelDescription>Your teambition organization 
id.</S.LabelDescription>}
-    >
-      <InputGroup placeholder="Your TenantId" value={value} 
onChange={handleChange} />
-    </FormGroup>
-  );
-};
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx 
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
deleted file mode 100644
index 613f48b87..000000000
--- 
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
+++ /dev/null
@@ -1,76 +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.
- *
- */
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useEffect } from 'react';
-import { FormGroup, InputGroup } from '@blueprintjs/core';
-
-import * as S from './styled';
-
-interface Props {
-  name: string;
-  initialValue: string;
-  value: string;
-  error: string;
-  setValue: (value: string) => void;
-  setError: (error: string) => void;
-}
-
-export const ConnectionTenantType = ({ initialValue, value, setValue, setError 
}: Props) => {
-  useEffect(() => {
-    setValue(initialValue);
-  }, [initialValue]);
-
-  useEffect(() => {
-    setError(value ? '' : 'TenantType is required');
-  }, [value]);
-
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    setValue(e.target.value);
-  };
-
-  return (
-    <FormGroup
-      label={<S.Label>Tenant Type</S.Label>}
-      labelInfo={<S.LabelInfo>*</S.LabelInfo>}
-      subLabel={
-        <S.LabelDescription>
-          You do not need to enter the tenant type, because teambition only 
supports 'organization' type currently.
-        </S.LabelDescription>
-      }
-    >
-      <InputGroup disabled={true} placeholder="Your API Tenant Type" 
value={value} onChange={handleChange} />
-    </FormGroup>
-  );
-};
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 9d4406bba..36b7aeda8 100644
--- a/config-ui/src/plugins/register/webhook/components/create-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/create-dialog.tsx
@@ -21,7 +21,6 @@ import { InputGroup, Icon } from '@blueprintjs/core';
 
 import API from '@/api';
 import { Dialog, FormItem, CopyText, ExternalLink } from '@/components';
-import { useConnections } from '@/hooks';
 import { operator } from '@/utils';
 
 import * as S from '../styled';
@@ -44,8 +43,6 @@ export const CreateDialog = ({ isOpen, onCancel, 
onSubmitAfter }: Props) => {
     apiKey: '',
   });
 
-  const { onRefresh } = useConnections();
-
   const prefix = useMemo(() => `${window.location.origin}/api`, []);
 
   const handleSubmit = async () => {
@@ -88,7 +85,6 @@ export const CreateDialog = ({ isOpen, onCancel, 
onSubmitAfter }: Props) => {
 }'`,
         apiKey: res.apiKey,
       });
-      onRefresh('webhook');
       onSubmitAfter?.(res.id);
     }
   };
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 2479f1754..806a26d5e 100644
--- a/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx
@@ -20,7 +20,6 @@ import { useState } from 'react';
 
 import API from '@/api';
 import { Dialog, Message } from '@/components';
-import { useConnections } from '@/hooks';
 import { operator } from '@/utils';
 
 interface Props {
@@ -32,15 +31,12 @@ interface Props {
 export const DeleteDialog = ({ initialId, onCancel, onSubmitAfter }: Props) => 
{
   const [operating, setOperating] = useState(false);
 
-  const { onRefresh } = useConnections();
-
   const handleSubmit = async () => {
     const [success] = await operator(() => 
API.plugin.webhook.remove(initialId), {
       setOperating,
     });
 
     if (success) {
-      onRefresh('webhook');
       onSubmitAfter?.(initialId);
       onCancel();
     }
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 a9fb55cc7..f656cdf7c 100644
--- a/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx
+++ b/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx
@@ -21,7 +21,6 @@ import { InputGroup } from '@blueprintjs/core';
 
 import API from '@/api';
 import { Dialog, FormItem } from '@/components';
-import { useConnections } from '@/hooks';
 import { operator } from '@/utils';
 
 interface Props {
@@ -33,8 +32,6 @@ export const EditDialog = ({ initialId, onCancel }: Props) => 
{
   const [name, setName] = useState('');
   const [operating, setOperating] = useState(false);
 
-  const { onRefresh } = useConnections({ plugin: 'webhook' });
-
   useEffect(() => {
     (async () => {
       const res = await API.plugin.webhook.get(initialId);
@@ -48,7 +45,6 @@ export const EditDialog = ({ initialId, onCancel }: Props) => 
{
     });
 
     if (success) {
-      onRefresh('webhook');
       onCancel();
     }
   };
diff --git a/config-ui/src/plugins/register/webhook/config.ts 
b/config-ui/src/plugins/register/webhook/config.ts
index 25088545d..b0f74c680 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/plugins/register/webhook/config.ts
@@ -17,14 +17,12 @@
  */
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import Icon from './assets/icon.svg';
 
 export const WebhookConfig: PluginConfigType = {
   plugin: 'webhook',
   name: 'Webhook',
-  type: PluginType.Connection,
   icon: Icon,
   sort: 100,
   connection: {
diff --git a/config-ui/src/plugins/register/webhook/connection.tsx 
b/config-ui/src/plugins/register/webhook/connection.tsx
index ffde52da7..07e68224c 100644
--- a/config-ui/src/plugins/register/webhook/connection.tsx
+++ b/config-ui/src/plugins/register/webhook/connection.tsx
@@ -19,8 +19,9 @@
 import { useState } from 'react';
 import { Button, Intent } from '@blueprintjs/core';
 
+import { useAppSelector } from '@/app/hook';
 import { Buttons, Table, ColumnType, ExternalLink, IconButton } from 
'@/components';
-import { useConnections } from '@/hooks';
+import { selectConnections } from '@/features/connections';
 import { DOC_URL } from '@/release';
 
 import { CreateDialog, ViewDialog, EditDialog, DeleteDialog } from 
'./components';
@@ -40,7 +41,7 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, 
onDeleteAfter }: P
   const [type, setType] = useState<Type>();
   const [currentID, setCurrentID] = useState<ID>();
 
-  const { connections } = useConnections({ plugin: 'webhook' });
+  const connections = useAppSelector((state) => selectConnections(state, 
'webhook'));
 
   const handleHideDialog = () => {
     setType(undefined);
diff --git a/config-ui/src/plugins/register/zentao/config.tsx 
b/config-ui/src/plugins/register/zentao/config.tsx
index c5ee0ccb1..705225dc7 100644
--- a/config-ui/src/plugins/register/zentao/config.tsx
+++ b/config-ui/src/plugins/register/zentao/config.tsx
@@ -19,13 +19,11 @@
 import { DOC_URL } from '@/release';
 
 import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
 
 import { DBUrl } from './connection-fields';
 import Icon from './assets/icon.svg';
 
 export const ZenTaoConfig: PluginConfigType = {
-  type: PluginType.Connection,
   plugin: 'zentao',
   name: 'ZenTao',
   icon: Icon,
diff --git a/config-ui/src/plugins/types.ts b/config-ui/src/plugins/types.ts
index b79e6c7c7..941a4715e 100644
--- a/config-ui/src/plugins/types.ts
+++ b/config-ui/src/plugins/types.ts
@@ -16,13 +16,7 @@
  *
  */
 
-export enum PluginType {
-  Connection = 'connection',
-  Pipeline = 'pipeline',
-}
-
 export type PluginConfigType = {
-  type: PluginType;
   plugin: string;
   name: string;
   icon: string;
diff --git a/config-ui/src/plugins/utils.ts b/config-ui/src/plugins/utils.ts
index f58667ffd..23e7af5f4 100644
--- a/config-ui/src/plugins/utils.ts
+++ b/config-ui/src/plugins/utils.ts
@@ -19,7 +19,7 @@
 import PluginIcon from '@/images/plugin-icon.svg';
 
 import { PluginConfig } from './config';
-import { PluginConfigType, PluginType } from './types';
+import { PluginConfigType } from './types';
 
 export const getPluginScopeId = (plugin: string, scope: any) => {
   switch (plugin) {
@@ -46,7 +46,6 @@ export const getPluginConfig = (name: string): 
PluginConfigType => {
   let pluginConfig = PluginConfig.find((plugin) => plugin.plugin === name) as 
PluginConfigType;
   if (!pluginConfig) {
     pluginConfig = {
-      type: PluginType.Pipeline,
       plugin: name,
       name: name,
       icon: PluginIcon,
diff --git a/config-ui/src/routes/layout/layout.tsx 
b/config-ui/src/routes/layout/layout.tsx
index 9ad858571..cc5b4b260 100644
--- a/config-ui/src/routes/layout/layout.tsx
+++ b/config-ui/src/routes/layout/layout.tsx
@@ -16,14 +16,16 @@
  *
  */
 
-import { useRef } from 'react';
+import { useEffect, useRef } from 'react';
 import { useLoaderData, Outlet, useNavigate, useLocation } from 
'react-router-dom';
 import { CSSTransition } from 'react-transition-group';
 import { Menu, MenuItem, Navbar, Alignment } from '@blueprintjs/core';
 
+import { useAppDispatch } from '@/app/hook';
 import { Logo, ExternalLink, IconButton } from '@/components';
+import { init } from '@/features';
 import { DOC_URL } from '@/release';
-import { TipsContextProvider, TipsContextConsumer, ConnectionContextProvider } 
from '@/store';
+import { TipsContextProvider, TipsContextConsumer } from '@/store';
 
 import DashboardIcon from '@/images/icons/dashboard.svg';
 import FileIcon from '@/images/icons/file.svg';
@@ -39,6 +41,12 @@ import './tips-transition.css';
 export const Layout = () => {
   const { version } = useLoaderData() as Awaited<ReturnType<typeof loader>>;
 
+  const dispatch = useAppDispatch();
+
+  useEffect(() => {
+    dispatch(init());
+  }, []);
+
   const navigate = useNavigate();
   const { pathname } = useLocation();
 
@@ -65,101 +73,99 @@ export const Layout = () => {
     <TipsContextProvider>
       <TipsContextConsumer>
         {({ tips, setTips }) => (
-          <ConnectionContextProvider>
-            <S.Wrapper>
-              <S.Sider>
-                <Logo />
-                <Menu className="menu">
-                  {menu.map((it) => {
-                    const paths = [it.path, ...(it.children ?? []).map((cit) 
=> cit.path)];
-                    const active = !!paths.find((path) => 
pathname.includes(path));
-                    return (
-                      <MenuItem
-                        key={it.key}
-                        className="menu-item"
-                        text={it.title}
-                        icon={it.icon}
-                        active={active}
-                        onClick={() => handlePushPath(it)}
-                      >
-                        {it.children?.map((cit) => (
-                          <MenuItem
-                            key={cit.key}
-                            className="sub-menu-item"
-                            text={
-                              <S.SiderMenuItem>
-                                <span>{cit.title}</span>
-                              </S.SiderMenuItem>
-                            }
-                            icon={cit.icon}
-                            active={pathname.includes(cit.path)}
-                            disabled={cit.disabled}
-                            onClick={() => handlePushPath(cit)}
-                          />
-                        ))}
-                      </MenuItem>
-                    );
-                  })}
-                </Menu>
-                <div className="copyright">
-                  <div>Apache 2.0 License</div>
-                  <div className="version">{version}</div>
-                </div>
-              </S.Sider>
-              <S.Main>
-                <S.Header>
-                  <Navbar.Group align={Alignment.RIGHT}>
-                    <S.DashboardIcon>
-                      <ExternalLink link={getGrafanaUrl()}>
-                        <img src={DashboardIcon} alt="dashboards" />
-                        <span>Dashboards</span>
-                      </ExternalLink>
-                    </S.DashboardIcon>
-                    <Navbar.Divider />
-                    <ExternalLink link={DOC_URL.TUTORIAL}>
-                      <img src={FileIcon} alt="documents" />
-                      <span>Docs</span>
-                    </ExternalLink>
-                    <Navbar.Divider />
-                    <ExternalLink link="/api/swagger/index.html">
-                      <img src={APIIcon} alt="api" />
-                      <span>API</span>
-                    </ExternalLink>
-                    <Navbar.Divider />
-                    <a
-                      href="https://github.com/apache/incubator-devlake";
-                      rel="noreferrer"
-                      target="_blank"
-                      className="navIconLink"
-                    >
-                      <img src={GitHubIcon} alt="github" />
-                      <span>GitHub</span>
-                    </a>
-                    <Navbar.Divider />
-                    <a
-                      
href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ";
-                      rel="noreferrer"
-                      target="_blank"
+          <S.Wrapper>
+            <S.Sider>
+              <Logo />
+              <Menu className="menu">
+                {menu.map((it) => {
+                  const paths = [it.path, ...(it.children ?? []).map((cit) => 
cit.path)];
+                  const active = !!paths.find((path) => 
pathname.includes(path));
+                  return (
+                    <MenuItem
+                      key={it.key}
+                      className="menu-item"
+                      text={it.title}
+                      icon={it.icon}
+                      active={active}
+                      onClick={() => handlePushPath(it)}
                     >
-                      <img src={SlackIcon} alt="slack" />
-                      <span>Slack</span>
-                    </a>
-                  </Navbar.Group>
-                </S.Header>
-                <S.Inner>
-                  <S.Content>
-                    <Outlet />
-                  </S.Content>
-                </S.Inner>
-                <CSSTransition in={!!tips} unmountOnExit timeout={300} 
nodeRef={tipsRef} classNames="tips">
-                  <S.Tips ref={tipsRef}>
-                    <div className="content">{tips}</div>
-                    <IconButton style={{ color: '#fff' }} icon="cross" 
tooltip="Close" onClick={() => setTips('')} />
-                  </S.Tips>
-                </CSSTransition>
-              </S.Main>
-            </S.Wrapper>
-          </ConnectionContextProvider>
+                      {it.children?.map((cit) => (
+                        <MenuItem
+                          key={cit.key}
+                          className="sub-menu-item"
+                          text={
+                            <S.SiderMenuItem>
+                              <span>{cit.title}</span>
+                            </S.SiderMenuItem>
+                          }
+                          icon={cit.icon}
+                          active={pathname.includes(cit.path)}
+                          disabled={cit.disabled}
+                          onClick={() => handlePushPath(cit)}
+                        />
+                      ))}
+                    </MenuItem>
+                  );
+                })}
+              </Menu>
+              <div className="copyright">
+                <div>Apache 2.0 License</div>
+                <div className="version">{version}</div>
+              </div>
+            </S.Sider>
+            <S.Main>
+              <S.Header>
+                <Navbar.Group align={Alignment.RIGHT}>
+                  <S.DashboardIcon>
+                    <ExternalLink link={getGrafanaUrl()}>
+                      <img src={DashboardIcon} alt="dashboards" />
+                      <span>Dashboards</span>
+                    </ExternalLink>
+                  </S.DashboardIcon>
+                  <Navbar.Divider />
+                  <ExternalLink link={DOC_URL.TUTORIAL}>
+                    <img src={FileIcon} alt="documents" />
+                    <span>Docs</span>
+                  </ExternalLink>
+                  <Navbar.Divider />
+                  <ExternalLink link="/api/swagger/index.html">
+                    <img src={APIIcon} alt="api" />
+                    <span>API</span>
+                  </ExternalLink>
+                  <Navbar.Divider />
+                  <a
+                    href="https://github.com/apache/incubator-devlake";
+                    rel="noreferrer"
+                    target="_blank"
+                    className="navIconLink"
+                  >
+                    <img src={GitHubIcon} alt="github" />
+                    <span>GitHub</span>
+                  </a>
+                  <Navbar.Divider />
+                  <a
+                    
href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ";
+                    rel="noreferrer"
+                    target="_blank"
+                  >
+                    <img src={SlackIcon} alt="slack" />
+                    <span>Slack</span>
+                  </a>
+                </Navbar.Group>
+              </S.Header>
+              <S.Inner>
+                <S.Content>
+                  <Outlet />
+                </S.Content>
+              </S.Inner>
+              <CSSTransition in={!!tips} unmountOnExit timeout={300} 
nodeRef={tipsRef} classNames="tips">
+                <S.Tips ref={tipsRef}>
+                  <div className="content">{tips}</div>
+                  <IconButton style={{ color: '#fff' }} icon="cross" 
tooltip="Close" onClick={() => setTips('')} />
+                </S.Tips>
+              </CSSTransition>
+            </S.Main>
+          </S.Wrapper>
         )}
       </TipsContextConsumer>
     </TipsContextProvider>
diff --git a/config-ui/src/store/connections/context.tsx 
b/config-ui/src/store/connections/context.tsx
deleted file mode 100644
index 42e213dea..000000000
--- a/config-ui/src/store/connections/context.tsx
+++ /dev/null
@@ -1,182 +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 { PageLoading } from '@/components';
-
-import API from '@/api';
-import type { PluginConfigType } from '@/plugins';
-import { PluginConfig, PluginType } from '@/plugins';
-
-import type { ConnectionItemType } from './types';
-import { ConnectionStatusEnum } from './types';
-
-export const ConnectionContext = React.createContext<{
-  connections: ConnectionItemType[];
-  onGet: (unique: string) => ConnectionItemType;
-  onTest: (unique: string) => void;
-  onRefresh: (plugin?: string) => void;
-}>(undefined!);
-
-interface Props {
-  children?: React.ReactNode;
-}
-
-export const ConnectionContextProvider = ({ children, ...props }: Props) => {
-  const [loading, setLoading] = useState(true);
-  const [connections, setConnections] = useState<ConnectionItemType[]>([]);
-
-  const plugins = useMemo(() => PluginConfig.filter((p) => p.type === 
PluginType.Connection), []);
-
-  const queryConnection = async (plugin: string) => {
-    try {
-      const res = await API.connection.list(plugin);
-      const { name, icon, isBeta, scopeConfig } = plugins.find((p) => p.plugin 
=== plugin) as PluginConfigType;
-
-      return res.map((connection) => ({
-        ...connection,
-        plugin,
-        pluginName: name,
-        icon,
-        isBeta: isBeta ?? false,
-        entities: scopeConfig?.entities ?? [],
-      }));
-    } catch {
-      return [];
-    }
-  };
-
-  const testConnection = async ({
-    plugin,
-    endpoint,
-    proxy,
-    token,
-    username,
-    password,
-    authMethod,
-    secretKey,
-    appId,
-    dbUrl,
-  }: ConnectionItemType) => {
-    try {
-      const res = await API.connection.test(plugin, {
-        endpoint,
-        proxy,
-        token,
-        username,
-        password,
-        authMethod,
-        secretKey,
-        appId,
-        dbUrl,
-      });
-      return res.success ? ConnectionStatusEnum.ONLINE : 
ConnectionStatusEnum.OFFLINE;
-    } catch {
-      return ConnectionStatusEnum.OFFLINE;
-    }
-  };
-
-  const transformConnection = (connections: Omit<ConnectionItemType, 'unique' 
| 'status'>[]) => {
-    return connections.map((it) => ({
-      unique: `${it.plugin}-${it.id}`,
-      plugin: it.plugin,
-      pluginName: it.pluginName,
-      id: it.id,
-      name: it.name,
-      status: ConnectionStatusEnum.NULL,
-      icon: it.icon,
-      isBeta: it.isBeta,
-      entities: it.entities,
-      endpoint: it.endpoint,
-      proxy: it.proxy,
-      token: it.token,
-      username: it.username,
-      password: it.password,
-      authMethod: it.authMethod,
-      secretKey: it.secretKey,
-      appId: it.appId,
-      dbUrl: it.dbUrl,
-    }));
-  };
-
-  const handleGet = (unique: string) => {
-    return connections.find((cs) => cs.unique === unique) as 
ConnectionItemType;
-  };
-
-  const handleTest = async (unique: string) => {
-    setConnections((connections) =>
-      connections.map((cs) =>
-        cs.unique === unique
-          ? {
-              ...cs,
-              status: ConnectionStatusEnum.TESTING,
-            }
-          : cs,
-      ),
-    );
-
-    const connection = handleGet(unique);
-    const status = await testConnection(connection);
-
-    setConnections((connections) =>
-      connections.map((cs) =>
-        cs.unique === unique
-          ? {
-              ...cs,
-              status,
-            }
-          : cs,
-      ),
-    );
-  };
-
-  const handleRefresh = async (plugin?: string) => {
-    if (plugin) {
-      const res = await queryConnection(plugin);
-      setConnections([...connections.filter((cs) => cs.plugin !== plugin), 
...transformConnection(res)]);
-      return;
-    }
-
-    const res = await Promise.all(plugins.map((cs) => 
queryConnection(cs.plugin)));
-
-    setConnections(transformConnection(res.flat()));
-    setLoading(false);
-  };
-
-  useEffect(() => {
-    handleRefresh();
-  }, []);
-
-  if (loading) {
-    return <PageLoading />;
-  }
-
-  return (
-    <ConnectionContext.Provider
-      value={{
-        connections,
-        onGet: handleGet,
-        onTest: handleTest,
-        onRefresh: handleRefresh,
-      }}
-    >
-      {children}
-    </ConnectionContext.Provider>
-  );
-};
diff --git a/config-ui/src/store/index.ts b/config-ui/src/store/index.ts
index a247f9b21..378c0c717 100644
--- a/config-ui/src/store/index.ts
+++ b/config-ui/src/store/index.ts
@@ -16,5 +16,4 @@
  *
  */
 
-export * from './connections';
 export * from './tips';
diff --git a/config-ui/src/store/connections/types.ts 
b/config-ui/src/types/connection.ts
similarity index 87%
rename from config-ui/src/store/connections/types.ts
rename to config-ui/src/types/connection.ts
index 07475889a..6225fd63b 100644
--- a/config-ui/src/store/connections/types.ts
+++ b/config-ui/src/types/connection.ts
@@ -16,30 +16,28 @@
  *
  */
 
-export enum ConnectionStatusEnum {
+export enum IConnectionStatus {
+  IDLE = 'idle',
+  TESTING = 'testing',
   ONLINE = 'online',
   OFFLINE = 'offline',
-  TESTING = 'testing',
-  NULL = 'null',
 }
 
-export type ConnectionItemType = {
+export interface IConnection {
   unique: string;
   plugin: string;
   pluginName: string;
   id: ID;
   name: string;
-  status: ConnectionStatusEnum;
+  status: IConnectionStatus;
   icon: string;
   isBeta: boolean;
-  entities: string[];
   endpoint: string;
   proxy: string;
+  authMethod?: string;
   token?: string;
   username?: string;
   password?: string;
-  authMethod?: string;
   appId?: string;
   secretKey?: string;
-  dbUrl?: string;
-};
+}
diff --git a/config-ui/src/plugins/register/teambition/index.ts 
b/config-ui/src/types/index.ts
similarity index 96%
rename from config-ui/src/plugins/register/teambition/index.ts
rename to config-ui/src/types/index.ts
index de415db39..fcc98feb8 100644
--- a/config-ui/src/plugins/register/teambition/index.ts
+++ b/config-ui/src/types/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './config';
+export * from './connection';
diff --git a/config-ui/yarn.lock b/config-ui/yarn.lock
index 587e396f0..c6fbdf122 100644
--- a/config-ui/yarn.lock
+++ b/config-ui/yarn.lock
@@ -1452,6 +1452,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.9.2":
+  version: 7.23.2
+  resolution: "@babel/runtime@npm:7.23.2"
+  dependencies:
+    regenerator-runtime: ^0.14.0
+  checksum: 
6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb
+  languageName: node
+  linkType: hard
+
 "@babel/runtime@npm:^7.21.0":
   version: 7.22.5
   resolution: "@babel/runtime@npm:7.22.5"
@@ -2007,6 +2016,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@reduxjs/toolkit@npm:^1.9.7":
+  version: 1.9.7
+  resolution: "@reduxjs/toolkit@npm:1.9.7"
+  dependencies:
+    immer: ^9.0.21
+    redux: ^4.2.1
+    redux-thunk: ^2.4.2
+    reselect: ^4.1.8
+  peerDependencies:
+    react: ^16.9.0 || ^17.0.0 || ^18
+    react-redux: ^7.2.1 || ^8.0.2
+  peerDependenciesMeta:
+    react:
+      optional: true
+    react-redux:
+      optional: true
+  checksum: 
ac25dec73a5d2df9fc7fbe98c14ccc73919e5ee1d6f251db0d2ec8f90273f92ef39c26716704bf56b5a40189f72d94b4526dc3a8c7ac3986f5daf44442bcc364
+  languageName: node
+  linkType: hard
+
 "@remix-run/router@npm:1.7.1":
   version: 1.7.1
   resolution: "@remix-run/router@npm:1.7.1"
@@ -2052,6 +2081,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/hoist-non-react-statics@npm:^3.3.1":
+  version: 3.3.3
+  resolution: "@types/hoist-non-react-statics@npm:3.3.3"
+  dependencies:
+    "@types/react": "*"
+    hoist-non-react-statics: ^3.3.0
+  checksum: 
107ac20ab36acdc83fb6bfca901e6f4f11307a0a307099c31ecf2a9875f8abffd731a2e1ee793162307e8aaee48fe9fd8d4e034fce88d5da480bc4178a3fc8d7
+  languageName: node
+  linkType: hard
+
 "@types/js-cookie@npm:^2.x.x":
   version: 2.2.7
   resolution: "@types/js-cookie@npm:2.2.7"
@@ -2185,6 +2224,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/use-sync-external-store@npm:^0.0.3":
+  version: 0.0.3
+  resolution: "@types/use-sync-external-store@npm:0.0.3"
+  checksum: 
161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/eslint-plugin@npm:^5.5.0":
   version: 5.55.0
   resolution: "@typescript-eslint/eslint-plugin@npm:5.55.0"
@@ -3016,6 +3062,7 @@ __metadata:
     "@blueprintjs/datetime2": ^1.0.10
     "@blueprintjs/popover2": ^2.0.10
     "@blueprintjs/select": ^5.0.10
+    "@reduxjs/toolkit": ^1.9.7
     "@types/file-saver": ^2.0.5
     "@types/node": ^18.15.1
     "@types/react": ^18.0.24
@@ -3045,8 +3092,10 @@ __metadata:
     react-copy-to-clipboard: ^5.1.0
     react-dom: 17.0.2
     react-is: ^18.2.0
+    react-redux: ^8.1.3
     react-router-dom: ^6.14.1
     react-transition-group: ^4.4.5
+    redux: ^4.2.1
     styled-components: ^5.3.6
     typescript: ^4.9.4
     vite: ^4.1.4
@@ -4372,7 +4421,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0":
+"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, 
hoist-non-react-statics@npm:^3.3.2":
   version: 3.3.2
   resolution: "hoist-non-react-statics@npm:3.3.2"
   dependencies:
@@ -4450,6 +4499,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"immer@npm:^9.0.21":
+  version: 9.0.21
+  resolution: "immer@npm:9.0.21"
+  checksum: 
70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432
+  languageName: node
+  linkType: hard
+
 "import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1":
   version: 3.3.0
   resolution: "import-fresh@npm:3.3.0"
@@ -5824,7 +5880,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-is@npm:^18.2.0":
+"react-is@npm:^18.0.0, react-is@npm:^18.2.0":
   version: 18.2.0
   resolution: "react-is@npm:18.2.0"
   checksum: 
e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e
@@ -5845,6 +5901,38 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-redux@npm:^8.1.3":
+  version: 8.1.3
+  resolution: "react-redux@npm:8.1.3"
+  dependencies:
+    "@babel/runtime": ^7.12.1
+    "@types/hoist-non-react-statics": ^3.3.1
+    "@types/use-sync-external-store": ^0.0.3
+    hoist-non-react-statics: ^3.3.2
+    react-is: ^18.0.0
+    use-sync-external-store: ^1.0.0
+  peerDependencies:
+    "@types/react": ^16.8 || ^17.0 || ^18.0
+    "@types/react-dom": ^16.8 || ^17.0 || ^18.0
+    react: ^16.8 || ^17.0 || ^18.0
+    react-dom: ^16.8 || ^17.0 || ^18.0
+    react-native: ">=0.59"
+    redux: ^4 || ^5.0.0-beta.0
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    "@types/react-dom":
+      optional: true
+    react-dom:
+      optional: true
+    react-native:
+      optional: true
+    redux:
+      optional: true
+  checksum: 
192ea6f6053148ec80a4148ec607bc259403b937e515f616a1104ca5ab357e97e98b8245ed505a17afee67a72341d4a559eaca9607968b4a422aa9b44ba7eb89
+  languageName: node
+  linkType: hard
+
 "react-refresh@npm:^0.14.0":
   version: 0.14.0
   resolution: "react-refresh@npm:0.14.0"
@@ -5912,6 +6000,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"redux-thunk@npm:^2.4.2":
+  version: 2.4.2
+  resolution: "redux-thunk@npm:2.4.2"
+  peerDependencies:
+    redux: ^4
+  checksum: 
c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c
+  languageName: node
+  linkType: hard
+
+"redux@npm:^4.2.1":
+  version: 4.2.1
+  resolution: "redux@npm:4.2.1"
+  dependencies:
+    "@babel/runtime": ^7.9.2
+  checksum: 
f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd
+  languageName: node
+  linkType: hard
+
 "regenerate-unicode-properties@npm:^10.1.0":
   version: 10.1.0
   resolution: "regenerate-unicode-properties@npm:10.1.0"
@@ -5935,6 +6041,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"regenerator-runtime@npm:^0.14.0":
+  version: 0.14.0
+  resolution: "regenerator-runtime@npm:0.14.0"
+  checksum: 
1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3
+  languageName: node
+  linkType: hard
+
 "regenerator-transform@npm:^0.15.1":
   version: 0.15.1
   resolution: "regenerator-transform@npm:0.15.1"
@@ -5980,6 +6093,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"reselect@npm:^4.1.8":
+  version: 4.1.8
+  resolution: "reselect@npm:4.1.8"
+  checksum: 
a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e
+  languageName: node
+  linkType: hard
+
 "resize-observer-polyfill@npm:^1.5.1":
   version: 1.5.1
   resolution: "resize-observer-polyfill@npm:1.5.1"
@@ -6809,6 +6929,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"use-sync-external-store@npm:^1.0.0":
+  version: 1.2.0
+  resolution: "use-sync-external-store@npm:1.2.0"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0
+  checksum: 
5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a
+  languageName: node
+  linkType: hard
+
 "util-deprecate@npm:^1.0.1":
   version: 1.0.2
   resolution: "util-deprecate@npm:1.0.2"


Reply via email to