fix: some bugs for scope config (#7452)

diff --git a/config-ui/src/components/action/icon-button/index.tsx b/config-ui/src/components/action/icon-button/index.tsx
index 04950c3..fb63f60 100644
--- a/config-ui/src/components/action/icon-button/index.tsx
+++ b/config-ui/src/components/action/icon-button/index.tsx
@@ -20,16 +20,14 @@
 import type { ButtonProps } from 'antd';
 import { Tooltip, Button } from 'antd';
 
-interface Props extends Pick<ButtonProps, 'type'> {
-  icon: React.ReactNode;
+interface Props extends Pick<ButtonProps, 'icon' | 'type' | 'size' | 'onClick'> {
   helptip: string;
-  onClick?: React.MouseEventHandler<HTMLElement> | undefined;
 }
 
-export const IconButton = forwardRef(function ({ icon, helptip, type, onClick }: Props, ref?: Ref<HTMLElement>) {
+export const IconButton = forwardRef(function ({ helptip, ...props }: Props, ref?: Ref<HTMLElement>) {
   return (
     <Tooltip title={helptip}>
-      <Button ref={ref} type={type} icon={icon} onClick={onClick} />
+      <Button ref={ref} {...props} />
     </Tooltip>
   );
 });
diff --git a/config-ui/src/plugins/components/scope-config-form/index.tsx b/config-ui/src/plugins/components/scope-config-form/index.tsx
index d3519d1..2466e9b 100644
--- a/config-ui/src/plugins/components/scope-config-form/index.tsx
+++ b/config-ui/src/plugins/components/scope-config-form/index.tsx
@@ -106,7 +106,7 @@
           : API.scopeConfig.update(plugin, connectionId, scopeConfigId, { name, entities, ...transformation }),
       {
         setOperating,
-        hideToast: !!scopeConfigId,
+        hideSuccessToast: !!scopeConfigId,
         formatMessage: () => 'Create scope config successful.',
       },
     );
@@ -138,7 +138,12 @@
               description="Give this Scope Config a unique name so that you can identify it in the future."
               required
             >
-              <Input placeholder="My Scope Config 1" value={name} onChange={(e) => setName(e.target.value)} />
+              <Input
+                placeholder="My Scope Config 1"
+                maxLength={40}
+                value={name}
+                onChange={(e) => setName(e.target.value)}
+              />
             </Block>
           </Card>
           <Card>
diff --git a/config-ui/src/plugins/components/scope-config/index.tsx b/config-ui/src/plugins/components/scope-config/index.tsx
index 3ef2afd..61ec1dc 100644
--- a/config-ui/src/plugins/components/scope-config/index.tsx
+++ b/config-ui/src/plugins/components/scope-config/index.tsx
@@ -22,7 +22,7 @@
 import styled from 'styled-components';
 
 import API from '@/api';
-import { Message } from '@/components';
+import { IconButton, Message } from '@/components';
 import { operator } from '@/utils';
 
 import { PluginName } from '../plugin-name';
@@ -38,7 +38,7 @@
   scopeName: string;
   id?: ID;
   name?: string;
-  onSuccess?: (id?: ID) => void;
+  onSuccess?: (id?: ID, hideToast?: boolean) => void;
 }
 
 export const ScopeConfig = ({ plugin, connectionId, scopeId, scopeName, id, name, onSuccess }: Props) => {
@@ -83,7 +83,7 @@
 
     if (success) {
       handleHideDialog();
-      onSuccess?.(id);
+      onSuccess?.(id, type === 'duplicate');
     }
   };
 
@@ -95,15 +95,24 @@
   return (
     <Wrapper>
       <span>{id ? name : 'N/A'}</span>
-      <Button
+      <IconButton
+        icon={<LinkOutlined />}
+        helptip="Associate Scope Config"
         size="small"
         type="link"
-        icon={<LinkOutlined />}
         onClick={() => {
           setType('associate');
         }}
       />
-      {id && <Button size="small" type="link" icon={<EditOutlined />} onClick={handleCheckScopeConfig} />}
+      {id && (
+        <IconButton
+          icon={<EditOutlined />}
+          helptip=" Edit Scope Config"
+          type="link"
+          size="small"
+          onClick={handleCheckScopeConfig}
+        />
+      )}
       {type === 'associate' && (
         <Modal
           open
@@ -172,7 +181,7 @@
           <Message content="The change will apply to all following projects:" />
           <ul style={{ margin: '15px 0 30px 30px' }}>
             {relatedProjects.map((it) => (
-              <li style={{ color: colorPrimary }}>
+              <li key={it.name} style={{ color: colorPrimary }}>
                 {it.name}: {it.scopes.map((sc) => sc.scopeName).join(',')}
               </li>
             ))}
diff --git a/config-ui/src/routes/blueprint/connection-detail/index.tsx b/config-ui/src/routes/blueprint/connection-detail/index.tsx
index 7b3fa96..d0c66d4 100644
--- a/config-ui/src/routes/blueprint/connection-detail/index.tsx
+++ b/config-ui/src/routes/blueprint/connection-detail/index.tsx
@@ -20,15 +20,16 @@
 import { useNavigate, useParams } from 'react-router-dom';
 import { Helmet } from 'react-helmet';
 import { DeleteOutlined, FormOutlined } from '@ant-design/icons';
-import { Flex, Table, Popconfirm, Modal, Button } from 'antd';
+import { Flex, Popconfirm, Modal, Button } from 'antd';
 
 import API from '@/api';
 import { PageLoading, PageHeader, ExternalLink } from '@/components';
 import { PATHS } from '@/config';
 import { useRefreshData } from '@/hooks';
-import { ScopeConfig, DataScopeSelect, getPluginScopeId } from '@/plugins';
+import { DataScopeSelect } from '@/plugins';
 import { operator } from '@/utils';
 
+import { BlueprintConnectionDetailTable } from './table';
 import * as S from './styled';
 
 const brandName = import.meta.env.DEVLAKE_BRAND_NAME ?? 'DevLake';
@@ -60,13 +61,6 @@
       API.connection.get(plugin, connectionId),
     ]);
 
-    const scopeIds =
-      blueprint.connections
-        .find((cs) => cs.pluginName === plugin && cs.connectionId === +connectionId)
-        ?.scopes?.map((sc: any) => sc.scopeId) ?? [];
-
-    const scopes = await Promise.all(scopeIds.map((scopeId) => API.scope.get(plugin, connectionId, scopeId)));
-
     return {
       blueprint,
       connection: {
@@ -75,12 +69,10 @@
         id: +connectionId,
         name: connection.name,
       },
-      scopes: scopes.map((sc) => ({
-        id: getPluginScopeId(plugin, sc.scope),
-        name: sc.scope.fullName ?? sc.scope.name,
-        scopeConfigId: sc.scopeConfig?.id,
-        scopeConfigName: sc.scopeConfig?.name,
-      })),
+      scopeIds:
+        blueprint.connections
+          .find((cs) => cs.pluginName === plugin && cs.connectionId === +connectionId)
+          ?.scopes?.map((sc: any) => sc.scopeId) ?? [],
     };
   }, [version, pname, bid]);
 
@@ -88,7 +80,7 @@
     return <PageLoading />;
   }
 
-  const { blueprint, connection, scopes } = data;
+  const { blueprint, connection, scopeIds } = data;
 
   const handleShowDataScope = () => setOpen(true);
   const handleHideDataScope = () => setOpen(false);
@@ -179,26 +171,6 @@
     }
   };
 
-  const handleChangeScopeConfig = () => {
-    modal.success({
-      closable: true,
-      centered: true,
-      width: 550,
-      title: 'Scope Config Saved',
-      content: 'Please re-transform data to apply the updated scope config.',
-      footer: (
-        <div style={{ marginTop: 20, textAlign: 'center' }}>
-          <Button type="primary" loading={operating} onClick={() => handleRun({ skipCollectors: true })}>
-            Re-transform now
-          </Button>
-        </div>
-      ),
-      onCancel: () => {
-        setVersion(version + 1);
-      },
-    });
-  };
-
   return (
     <PageHeader
       breadcrumbs={
@@ -251,32 +223,12 @@
             Manage Data Scope
           </Button>
         </Flex>
-        <Table
-          rowKey="id"
-          size="middle"
-          columns={[
-            {
-              title: 'Data Scope',
-              dataIndex: 'name',
-              key: 'name',
-            },
-            {
-              title: 'Scope Config',
-              key: 'scopeConfig',
-              render: (_, { id, name, scopeConfigId, scopeConfigName }) => (
-                <ScopeConfig
-                  plugin={plugin}
-                  connectionId={connectionId}
-                  scopeId={id}
-                  scopeName={name}
-                  id={scopeConfigId}
-                  name={scopeConfigName}
-                  onSuccess={handleChangeScopeConfig}
-                />
-              ),
-            },
-          ]}
-          dataSource={scopes}
+        <BlueprintConnectionDetailTable
+          plugin={plugin}
+          connectionId={connectionId}
+          scopeIds={scopeIds}
+          operating={operating}
+          onRun={handleRun}
         />
       </Flex>
       <Modal open={open} width={820} centered title="Manage Data Scope" footer={null} onCancel={handleHideDataScope}>
@@ -284,7 +236,7 @@
           plugin={connection.plugin}
           connectionId={connection.id}
           showWarning
-          initialScope={scopes}
+          initialScope={scopeIds.map((id) => ({ id }))}
           onCancel={handleHideDataScope}
           onSubmit={handleChangeDataScope}
         />
diff --git a/config-ui/src/routes/blueprint/connection-detail/table.tsx b/config-ui/src/routes/blueprint/connection-detail/table.tsx
new file mode 100644
index 0000000..35de59a
--- /dev/null
+++ b/config-ui/src/routes/blueprint/connection-detail/table.tsx
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useState } from 'react';
+import { Table, Modal, Button } from 'antd';
+
+import API from '@/api';
+import { useRefreshData } from '@/hooks';
+import { getPluginScopeId, ScopeConfig } from '@/plugins';
+
+interface Props {
+  plugin: string;
+  connectionId: ID;
+  scopeIds: ID[];
+  operating: boolean;
+  onRun: (params: { skipCollectors: boolean }) => void;
+}
+
+export const BlueprintConnectionDetailTable = ({ plugin, connectionId, scopeIds, operating, onRun }: Props) => {
+  const [version, setVersion] = useState(1);
+
+  const { ready, data } = useRefreshData(async () => {
+    const scopes = await Promise.all(scopeIds.map((scopeId) => API.scope.get(plugin, connectionId, scopeId)));
+    return scopes.map((sc) => ({
+      id: getPluginScopeId(plugin, sc.scope),
+      name: sc.scope.fullName ?? sc.scope.name,
+      scopeConfigId: sc.scopeConfig?.id,
+      scopeConfigName: sc.scopeConfig?.name,
+    }));
+  }, [version]);
+
+  const [modal, contextHolder] = Modal.useModal();
+
+  const handleChangeScopeConfig = () => {
+    modal.success({
+      closable: true,
+      centered: true,
+      width: 550,
+      title: 'Scope Config Saved',
+      content: 'Please re-transform data to apply the updated scope config.',
+      footer: (
+        <div style={{ marginTop: 20, textAlign: 'center' }}>
+          <Button type="primary" loading={operating} onClick={() => onRun({ skipCollectors: true })}>
+            Re-transform now
+          </Button>
+        </div>
+      ),
+      onCancel: () => {
+        setVersion(version + 1);
+      },
+    });
+  };
+
+  return (
+    <>
+      <Table
+        loading={!ready}
+        rowKey="id"
+        size="middle"
+        columns={[
+          {
+            title: 'Data Scope',
+            dataIndex: 'name',
+            key: 'name',
+          },
+          {
+            title: 'Scope Config',
+            key: 'scopeConfig',
+            render: (_, { id, name, scopeConfigId, scopeConfigName }) => (
+              <ScopeConfig
+                plugin={plugin}
+                connectionId={connectionId}
+                scopeId={id}
+                scopeName={name}
+                id={scopeConfigId}
+                name={scopeConfigName}
+                onSuccess={handleChangeScopeConfig}
+              />
+            ),
+          },
+        ]}
+        dataSource={data ?? []}
+      />
+      {contextHolder}
+    </>
+  );
+};
diff --git a/config-ui/src/routes/connection/connection.tsx b/config-ui/src/routes/connection/connection.tsx
index fbb9012..e6b2915 100644
--- a/config-ui/src/routes/connection/connection.tsx
+++ b/config-ui/src/routes/connection/connection.tsx
@@ -230,7 +230,18 @@
     }
   };
 
-  const handleScopeConfigChange = async (scopeConfigId?: ID) => {
+  const handleRun = async (pname: string, blueprintId: ID, data?: { skipCollectors?: boolean; fullSync?: boolean }) => {
+    const [success] = await operator(() => API.blueprint.trigger(blueprintId, data), {
+      setOperating,
+      hideToast: true,
+    });
+
+    if (success) {
+      window.open(PATHS.PROJECT(pname, { tab: 'status' }));
+    }
+  };
+
+  const handleScopeConfigChange = async (scopeConfigId?: ID, hideToast?: boolean) => {
     if (!scopeConfigId) {
       setVersion(version + 1);
       return;
@@ -239,7 +250,7 @@
     const [success, res] = await operator(() => API.scopeConfig.check(plugin, scopeConfigId), { hideToast: true });
 
     if (success) {
-      if (!res.projects) {
+      if (!res.projects || hideToast) {
         setVersion(version + 1);
         return;
       }
@@ -255,14 +266,14 @@
               The listed projects are impacted. Please re-transform the data to apply the updated scope config.
             </div>
             <ul>
-              {res.projects.map((it: any) => (
-                <li key={it.name} style={{ marginBottom: 10 }}>
+              {res.projects.map(({ name, blueprintId }: { name: string; blueprintId: ID }) => (
+                <li key={name} style={{ marginBottom: 10 }}>
                   <Space>
-                    <span>{it.name}</span>
+                    <span>{name}</span>
                     <Button
                       size="small"
                       type="link"
-                      onClick={() => navigate(PATHS.PROJECT(it.name, { tab: 'status' }))}
+                      onClick={() => handleRun(name, blueprintId, { skipCollectors: true })}
                     >
                       Re-transform Data
                     </Button>