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

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


The following commit(s) were added to refs/heads/main by this push:
     new b6bd5da93 fix(config-ui): some bugs (#4084)
b6bd5da93 is described below

commit b6bd5da93572f164e2e96546f921a41bb599edee
Author: 青湛 <[email protected]>
AuthorDate: Tue Jan 3 21:10:36 2023 +0800

    fix(config-ui): some bugs (#4084)
    
    * fix(config-ui): adjust the column count for gitlab and jenkins
    
    * fix(config-ui): the multi-selector scroll to top when the selectedItems 
changed
    
    * fix(config-ui): the selector content not fill
    
    * fix(config-ui): the connection default value not work
    
    * fix(config-ui): updated tasks will refresh page
    
    * fix(config-ui): unable to modify project name continuously
    
    * fix(config-ui): bp detail not show all entities on change scope
    
    * fix(config-ui): advacned mode bp cannot show
    
    * fix(config-ui): pipeline rerun path error
    
    * feat(config-ui): add github token real-time verification
    
    * fix(config-ui): show jenkins scope tips condition error
    
    * fix(config-ui): pipeline historical cannot auto refresh
---
 .../components/selector/multi-selector/index.tsx   |   7 +-
 .../src/components/selector/selector/index.tsx     |  10 +-
 .../src/{pages/pipeline/detail => hooks}/index.ts  |   2 +-
 config-ui/src/hooks/use-auto-refresh.ts            |  67 ++++++++++
 config-ui/src/layouts/base/styled.ts               |   1 -
 .../blueprint/detail/blueprint-detail-page.tsx     |  16 +--
 .../pages/blueprint/detail/blueprint-detail.tsx    |  63 ++++++----
 .../src/pages/blueprint/detail/components/index.ts |   1 -
 .../components/update-scope-dialog/index.tsx       |   5 +-
 .../pages/blueprint/detail/panel/configuration.tsx |   7 +-
 .../src/pages/blueprint/detail/panel/status.tsx    |  11 +-
 config-ui/src/pages/blueprint/detail/styled.ts     |   8 +-
 config-ui/src/pages/blueprint/detail/types.ts      |   1 +
 config-ui/src/pages/blueprint/detail/use-detail.ts |   7 +-
 .../form/components/github-token/index.tsx         | 136 +++++++++++++++++++++
 .../form/{ => components/github-token}/styled.ts   |  66 +++-------
 .../detail => connection/form/components}/index.ts |   2 +-
 config-ui/src/pages/connection/form/index.tsx      |  86 ++-----------
 config-ui/src/pages/connection/form/styled.ts      |  19 ---
 .../components/pipeline-list => pipeline}/api.ts   |   2 +
 .../components/historical}/index.tsx               |  79 ++++++------
 .../components/historical/styled.ts}               |  29 ++++-
 .../pages/pipeline/{detail => components}/index.ts |   2 +-
 config-ui/src/pages/pipeline/detail/api.ts         |   4 +-
 config-ui/src/pages/pipeline/detail/index.ts       |   2 +-
 .../src/pages/pipeline/detail/pipeline-detail.tsx  |  48 ++++++--
 .../types.ts => pipeline/detail/pipeline-info.tsx} |  23 ++--
 config-ui/src/pages/pipeline/detail/use-detail.ts  |  62 ++--------
 config-ui/src/pages/pipeline/index.ts              |   1 +
 config-ui/src/pages/project/detail/use-project.ts  |   2 +-
 .../plugins/common/data-scope/use-data-scope.ts    |  13 +-
 .../gitlab/components/miller-columns/index.tsx     |   2 +-
 .../jenkins/components/miller-columns/index.tsx    |   2 +-
 33 files changed, 449 insertions(+), 337 deletions(-)

diff --git a/config-ui/src/components/selector/multi-selector/index.tsx 
b/config-ui/src/components/selector/multi-selector/index.tsx
index 48fcda0c7..65a9f22d9 100644
--- a/config-ui/src/components/selector/multi-selector/index.tsx
+++ b/config-ui/src/components/selector/multi-selector/index.tsx
@@ -18,7 +18,7 @@
 
 import React, { useState, useEffect } from 'react';
 import { MenuItem, Checkbox, Intent } from '@blueprintjs/core';
-import { MultiSelect } from '@blueprintjs/select';
+import { MultiSelect2 } from '@blueprintjs/select';
 
 interface Props<T> {
   placeholder?: string;
@@ -98,11 +98,14 @@ export const MultiSelector = <T,>({
   };
 
   return (
-    <MultiSelect
+    <MultiSelect2
       fill
       resetOnSelect
       placeholder={placeholder ?? 'Select...'}
       items={items}
+      // https://github.com/palantir/blueprint/issues/3596
+      // set activeItem to null will fixed the scrollBar to top when the 
selectedItems changed
+      activeItem={null}
       selectedItems={selectedItems}
       itemRenderer={itemRenderer}
       tagRenderer={tagRenderer}
diff --git a/config-ui/src/components/selector/selector/index.tsx 
b/config-ui/src/components/selector/selector/index.tsx
index 2d284ee9c..0f334179d 100644
--- a/config-ui/src/components/selector/selector/index.tsx
+++ b/config-ui/src/components/selector/selector/index.tsx
@@ -17,8 +17,8 @@
  */
 
 import React, { useState, useEffect, useMemo } from 'react';
-import { MenuItem, Button } from '@blueprintjs/core';
-import { Select } from '@blueprintjs/select';
+import { MenuItem, Button, Alignment } from '@blueprintjs/core';
+import { Select2 } from '@blueprintjs/select';
 
 interface Props<T> {
   items: T[];
@@ -67,14 +67,14 @@ export const Selector = <T,>({
   };
 
   return (
-    <Select
+    <Select2
       items={items}
       activeItem={selectedItem}
       itemPredicate={itemPredicate}
       itemRenderer={itemRenderer}
       onItemSelect={handleItemSelect}
     >
-      <Button outlined small fill rightIcon="double-caret-vertical" 
text={btnText} />
-    </Select>
+      <Button outlined small fill alignText={Alignment.LEFT} 
rightIcon="double-caret-vertical" text={btnText} />
+    </Select2>
   );
 };
diff --git a/config-ui/src/pages/pipeline/detail/index.ts 
b/config-ui/src/hooks/index.ts
similarity index 95%
copy from config-ui/src/pages/pipeline/detail/index.ts
copy to config-ui/src/hooks/index.ts
index 07d867746..bd3a94b28 100644
--- a/config-ui/src/pages/pipeline/detail/index.ts
+++ b/config-ui/src/hooks/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './pipeline-detail';
+export * from './use-auto-refresh';
diff --git a/config-ui/src/hooks/use-auto-refresh.ts 
b/config-ui/src/hooks/use-auto-refresh.ts
new file mode 100644
index 000000000..33059964c
--- /dev/null
+++ b/config-ui/src/hooks/use-auto-refresh.ts
@@ -0,0 +1,67 @@
+/*
+ * 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, useEffect, useMemo, useRef } from 'react';
+
+export const useAutoRefresh = <T>(
+  request: () => Promise<T>,
+  deps: React.DependencyList = [],
+  option?: {
+    cancel?: (data?: T) => boolean;
+    interval?: number;
+  },
+) => {
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState<T>();
+
+  const timer = useRef<any>();
+
+  useEffect(() => {
+    setLoading(true);
+    request()
+      .then((data: T) => {
+        setData(data);
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [...deps]);
+
+  useEffect(() => {
+    timer.current = setInterval(() => {
+      request().then((data: T) => {
+        setData(data);
+      });
+    }, option?.interval ?? 5000);
+    return () => clearInterval(timer.current);
+  }, [...deps]);
+
+  useEffect(() => {
+    if (option?.cancel?.(data)) {
+      clearInterval(timer.current);
+    }
+  }, [data]);
+
+  return useMemo(
+    () => ({
+      loading,
+      data,
+    }),
+    [loading, data],
+  );
+};
diff --git a/config-ui/src/layouts/base/styled.ts 
b/config-ui/src/layouts/base/styled.ts
index 8ef7405d4..6fc945334 100644
--- a/config-ui/src/layouts/base/styled.ts
+++ b/config-ui/src/layouts/base/styled.ts
@@ -77,7 +77,6 @@ export const Sider = styled.div`
 `;
 
 export const Inner = styled.div`
-  position: relative;
   display: flex;
   flex-direction: column;
   flex: auto;
diff --git a/config-ui/src/pages/blueprint/detail/blueprint-detail-page.tsx 
b/config-ui/src/pages/blueprint/detail/blueprint-detail-page.tsx
index 48a472a1e..bfbde9404 100644
--- a/config-ui/src/pages/blueprint/detail/blueprint-detail-page.tsx
+++ b/config-ui/src/pages/blueprint/detail/blueprint-detail-page.tsx
@@ -16,28 +16,19 @@
  *
  */
 
-import React, { useMemo } from 'react';
+import React from 'react';
 import { useParams } from 'react-router-dom';
 
 import { PageHeader, PageLoading } from '@/components';
-import { Plugins } from '@/plugins';
 
 import { useDetail } from './use-detail';
 import { BlueprintDetail } from './blueprint-detail';
 
-import * as S from './styled';
-
 export const BlueprintDetailPage = () => {
   const { id } = useParams<{ id: string }>();
 
   const { loading, blueprint } = useDetail({ id });
 
-  const showJenkinsTips = useMemo(() => {
-    const jenkins = blueprint && blueprint.settings.connections.find((cs) => 
cs.plugin === Plugins.Jenkins);
-
-    return !jenkins?.scopes.length;
-  }, [blueprint]);
-
   if (loading || !blueprint) {
     return <PageLoading />;
   }
@@ -50,11 +41,6 @@ export const BlueprintDetailPage = () => {
       ]}
     >
       <BlueprintDetail id={blueprint.id} />
-      {showJenkinsTips && (
-        <S.JenkinsTips>
-          <p>Please add the "Jenkins jobs" to collect before this Blueprint 
can run again.</p>
-        </S.JenkinsTips>
-      )}
     </PageHeader>
   );
 };
diff --git a/config-ui/src/pages/blueprint/detail/blueprint-detail.tsx 
b/config-ui/src/pages/blueprint/detail/blueprint-detail.tsx
index cc558e0db..6f0edd139 100644
--- a/config-ui/src/pages/blueprint/detail/blueprint-detail.tsx
+++ b/config-ui/src/pages/blueprint/detail/blueprint-detail.tsx
@@ -16,52 +16,67 @@
  *
  */
 
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
 import type { TabId } from '@blueprintjs/core';
 import { Tabs, Tab } from '@blueprintjs/core';
 
 import { PageLoading } from '@/components';
+import { Plugins } from '@/plugins';
 
 import type { UseDetailProps } from './use-detail';
 import { useDetail } from './use-detail';
 import { Configuration } from './panel/configuration';
 import { Status } from './panel/status';
+import * as S from './styled';
 
 interface Props extends UseDetailProps {}
 
 export const BlueprintDetail = ({ id }: Props) => {
   const [activeTab, setActiveTab] = useState<TabId>('status');
 
-  const { loading, blueprint, pipelines, pipelineId, operating, onRun, 
onUpdate, onDelete, onRefresh } = useDetail({
+  const { loading, blueprint, pipelineId, operating, onRun, onUpdate, 
onDelete, onRefresh } = useDetail({
     id,
   });
 
+  const showJenkinsTips = useMemo(() => {
+    const jenkins = blueprint && blueprint.settings?.connections.find((cs) => 
cs.plugin === Plugins.Jenkins);
+    return jenkins && !jenkins.scopes.length;
+  }, [blueprint]);
+
   if (loading || !blueprint) {
     return <PageLoading />;
   }
 
   return (
-    <Tabs selectedTabId={activeTab} onChange={(at) => setActiveTab(at)}>
-      <Tab
-        id="status"
-        title="Status"
-        panel={
-          <Status
-            blueprint={blueprint}
-            pipelines={pipelines}
-            pipelineId={pipelineId}
-            operating={operating}
-            onRun={onRun}
-            onUpdate={onUpdate}
-            onDelete={onDelete}
-          />
-        }
-      />
-      <Tab
-        id="configuration"
-        title="Configuration"
-        panel={<Configuration blueprint={blueprint} operating={operating} 
onUpdate={onUpdate} onRefresh={onRefresh} />}
-      />
-    </Tabs>
+    <S.Wrapper>
+      <Tabs selectedTabId={activeTab} onChange={(at) => setActiveTab(at)}>
+        <Tab
+          id="status"
+          title="Status"
+          panel={
+            <Status
+              blueprint={blueprint}
+              pipelineId={pipelineId}
+              operating={operating}
+              onRun={onRun}
+              onUpdate={onUpdate}
+              onDelete={onDelete}
+            />
+          }
+        />
+        <Tab
+          id="configuration"
+          title="Configuration"
+          panel={
+            <Configuration blueprint={blueprint} operating={operating} 
onUpdate={onUpdate} onRefresh={onRefresh} />
+          }
+        />
+      </Tabs>
+      {showJenkinsTips && (
+        <S.JenkinsTips>
+          <p>Please add the "Jenkins jobs" to collect before this Blueprint 
can run again.</p>
+        </S.JenkinsTips>
+      )}
+    </S.Wrapper>
   );
 };
diff --git a/config-ui/src/pages/blueprint/detail/components/index.ts 
b/config-ui/src/pages/blueprint/detail/components/index.ts
index a68cbba59..a78ec47ea 100644
--- a/config-ui/src/pages/blueprint/detail/components/index.ts
+++ b/config-ui/src/pages/blueprint/detail/components/index.ts
@@ -20,4 +20,3 @@ export * from './update-name-dialog';
 export * from './update-policy-dialog';
 export * from './update-scope-dialog';
 export * from './update-transformation-dialog';
-export * from './pipeline-list';
diff --git 
a/config-ui/src/pages/blueprint/detail/components/update-scope-dialog/index.tsx 
b/config-ui/src/pages/blueprint/detail/components/update-scope-dialog/index.tsx
index 61e998a8e..1825e3ffc 100644
--- 
a/config-ui/src/pages/blueprint/detail/components/update-scope-dialog/index.tsx
+++ 
b/config-ui/src/pages/blueprint/detail/components/update-scope-dialog/index.tsx
@@ -32,7 +32,7 @@ interface Props {
 export const UpdateScopeDialog = ({ connection, onCancel, onSubmit }: Props) 
=> {
   if (!connection) return null;
 
-  const { plugin, connectionId, entities } = connection;
+  const { plugin, connectionId, entities, selectedEntites } = connection;
 
   const handleSaveScope = (sc: any) => {
     onSubmit({
@@ -51,6 +51,9 @@ export const UpdateScopeDialog = ({ connection, onCancel, 
onSubmit }: Props) =>
         plugin={plugin}
         connectionId={connectionId}
         entities={entities}
+        initialValues={{
+          entites: selectedEntites,
+        }}
         onCancel={onCancel}
         onSave={handleSaveScope}
       />
diff --git a/config-ui/src/pages/blueprint/detail/panel/configuration.tsx 
b/config-ui/src/pages/blueprint/detail/panel/configuration.tsx
index 591f83500..73baede66 100644
--- a/config-ui/src/pages/blueprint/detail/panel/configuration.tsx
+++ b/config-ui/src/pages/blueprint/detail/panel/configuration.tsx
@@ -63,7 +63,8 @@ export const Configuration = ({ blueprint, operating, 
onUpdate, onRefresh }: Pro
             icon: plugin.icon,
             name: plugin.name,
             connectionId: cs.connectionId,
-            entities: cs.scopes[0].entities,
+            entities: plugin.entities,
+            selectedEntites: cs.scopes[0].entities,
             plugin: cs.plugin,
             scopeIds: cs.scopes.map((sc: any) => sc.id),
           };
@@ -117,8 +118,8 @@ export const Configuration = ({ blueprint, operating, 
onUpdate, onRefresh }: Pro
         },
         {
           title: 'Data Entities',
-          dataIndex: 'entities',
-          key: 'entities',
+          dataIndex: 'selectedEntites',
+          key: 'selectedEntites',
           render: (val: string[]) => (
             <>
               {val.map((it) => (
diff --git a/config-ui/src/pages/blueprint/detail/panel/status.tsx 
b/config-ui/src/pages/blueprint/detail/panel/status.tsx
index 2047cdb0e..f7401e1cc 100644
--- a/config-ui/src/pages/blueprint/detail/panel/status.tsx
+++ b/config-ui/src/pages/blueprint/detail/panel/status.tsx
@@ -21,17 +21,14 @@ import { Button, Switch, Intent } from '@blueprintjs/core';
 import dayjs from 'dayjs';
 
 import { getCron } from '@/config';
-import type { PipelineType } from '@/pages';
-import { PipelineDetail } from '@/pages';
+import { PipelineInfo, PipelineHistorical } from '@/pages';
 
 import type { BlueprintType } from '../../types';
 
-import { PipelineList } from '../components';
 import * as S from '../styled';
 
 interface Props {
   blueprint: BlueprintType;
-  pipelines: PipelineType[];
   pipelineId?: ID;
   operating: boolean;
   onRun: () => void;
@@ -39,7 +36,7 @@ interface Props {
   onDelete: () => void;
 }
 
-export const Status = ({ blueprint, pipelines, pipelineId, operating, onRun, 
onUpdate, onDelete }: Props) => {
+export const Status = ({ blueprint, pipelineId, operating, onRun, onUpdate, 
onDelete }: Props) => {
   const cron = useMemo(() => getCron(blueprint.isManual, 
blueprint.cronConfig), [blueprint]);
 
   const handleRunNow = () => onRun();
@@ -84,11 +81,11 @@ export const Status = ({ blueprint, pipelines, pipelineId, 
operating, onRun, onU
       </div>
       <div className="block">
         <h3>Current Pipeline</h3>
-        <PipelineDetail id={pipelineId} />
+        <PipelineInfo id={pipelineId} />
       </div>
       <div className="block">
         <h3>Historical Pipelines</h3>
-        <PipelineList pipelines={pipelines} />
+        <PipelineHistorical blueprintId={blueprint.id} />
       </div>
     </S.StatusPanel>
   );
diff --git a/config-ui/src/pages/blueprint/detail/styled.ts 
b/config-ui/src/pages/blueprint/detail/styled.ts
index 9b480e23d..0a772d8a1 100644
--- a/config-ui/src/pages/blueprint/detail/styled.ts
+++ b/config-ui/src/pages/blueprint/detail/styled.ts
@@ -18,6 +18,10 @@
 
 import styled from 'styled-components';
 
+export const Wrapper = styled.div`
+  padding-bottom: 24px;
+`;
+
 export const ConfigurationPanel = styled.div`
   .top {
     display: flex;
@@ -98,10 +102,10 @@ export const StatusPanel = styled.div`
 `;
 
 export const JenkinsTips = styled.div`
-  position: absolute;
+  position: fixed;
   right: 0;
   bottom: 0;
-  left: 0;
+  left: 200px;
   background-color: #3c5088;
   display: flex;
   align-items: center;
diff --git a/config-ui/src/pages/blueprint/detail/types.ts 
b/config-ui/src/pages/blueprint/detail/types.ts
index 174e43525..872ebb001 100644
--- a/config-ui/src/pages/blueprint/detail/types.ts
+++ b/config-ui/src/pages/blueprint/detail/types.ts
@@ -24,5 +24,6 @@ export type ConfigConnectionItemType = {
   connectionId: ID;
   plugin: Plugins;
   entities: string[];
+  selectedEntites: string[];
   scopeIds: ID[];
 };
diff --git a/config-ui/src/pages/blueprint/detail/use-detail.ts 
b/config-ui/src/pages/blueprint/detail/use-detail.ts
index f4803ce30..15a4e9f04 100644
--- a/config-ui/src/pages/blueprint/detail/use-detail.ts
+++ b/config-ui/src/pages/blueprint/detail/use-detail.ts
@@ -21,7 +21,7 @@ import { useState, useEffect, useMemo } from 'react';
 import { Error } from '@/error';
 import { operator } from '@/utils';
 
-import type { BlueprintType, PipelineType } from '@/pages';
+import type { BlueprintType } from '@/pages';
 import * as API from './api';
 
 export interface UseDetailProps {
@@ -32,7 +32,6 @@ export const useDetail = ({ id }: UseDetailProps) => {
   const [loading, setLoading] = useState(false);
   const [operating, setOperating] = useState(false);
   const [blueprint, setBlueprint] = useState<BlueprintType>();
-  const [pipelines, setPipelines] = useState<PipelineType[]>([]);
   const [pipelineId, setPipelineId] = useState<ID>();
   const [, setError] = useState();
 
@@ -49,7 +48,6 @@ export const useDetail = ({ id }: UseDetailProps) => {
       }
 
       setBlueprint(bpRes);
-      setPipelines(plRes.pipelines);
       setPipelineId(plRes.pipelines?.[0]?.id);
     } finally {
       setLoading(false);
@@ -102,13 +100,12 @@ export const useDetail = ({ id }: UseDetailProps) => {
       loading,
       operating,
       blueprint,
-      pipelines,
       pipelineId,
       onRun: handleRun,
       onUpdate: handleUpdate,
       onDelete: handleDelete,
       onRefresh: getBlueprint,
     }),
-    [loading, operating, blueprint, pipelines, pipelineId],
+    [loading, operating, blueprint, pipelineId],
   );
 };
diff --git 
a/config-ui/src/pages/connection/form/components/github-token/index.tsx 
b/config-ui/src/pages/connection/form/components/github-token/index.tsx
new file mode 100644
index 000000000..82b5d7193
--- /dev/null
+++ b/config-ui/src/pages/connection/form/components/github-token/index.tsx
@@ -0,0 +1,136 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { InputGroup, Button, Intent } from '@blueprintjs/core';
+import { pick } from 'lodash';
+
+import { Plugins } from '@/plugins';
+
+import * as API from '../../api';
+
+import * as S from './styled';
+
+type TokenItem = {
+  value: string;
+  status: 'idle' | 'valid' | 'invalid';
+  from?: string;
+};
+
+interface Props {
+  form: any;
+  value?: string;
+  onChange?: (value: string) => void;
+}
+
+export const GitHubToken = ({ form, value, onChange }: Props) => {
+  const [tokens, setTokens] = useState<TokenItem[]>([]);
+
+  const testToken = async (token: string): Promise<TokenItem> => {
+    try {
+      const res = await API.testConnection(Plugins.GitHub, {
+        ...pick(form, ['endpoint', 'proxy']),
+        token,
+      });
+      return {
+        value: token,
+        status: 'valid',
+        from: res.login,
+      };
+    } catch {
+      return {
+        value: token,
+        status: 'invalid',
+      };
+    }
+  };
+
+  const checkTokens = async (value: string) => {
+    const res = await Promise.all((value ?? '').split(',').map((it) => 
testToken(it)));
+    setTokens(res);
+  };
+
+  useEffect(() => {
+    if (value) {
+      checkTokens(value);
+    }
+  }, []);
+
+  useEffect(() => {
+    onChange?.(tokens.map((it) => it.value).join(','));
+  }, [tokens]);
+
+  const handleCreateToken = () => {
+    setTokens([...tokens, { value: '', status: 'idle' }]);
+  };
+
+  const handleRemoveToken = (key: number) => {
+    setTokens(tokens.filter((_, i) => (i === key ? false : true)));
+  };
+
+  const handleChangeToken = (key: number, value: string) => {
+    setTokens(tokens.map((it, i) => (i === key ? { value, status: 'idle' } : 
it)));
+  };
+
+  const handleTestToken = async (key: number) => {
+    const token = tokens.find((_, i) => i === key) as TokenItem;
+
+    if (token.status === 'idle' && token.value) {
+      const res = await testToken(token.value);
+      setTokens((tokens) => tokens.map((it, i) => (i === key ? res : it)));
+    }
+  };
+
+  return (
+    <S.Wrapper>
+      <p>
+        Add one or more personal token(s) for authentication from you and your 
organization members. Multiple tokens can
+        help speed up the data collection process.{' '}
+      </p>
+      <p>
+        <a
+          
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token";
+          target="_blank"
+          rel="noreferrer"
+        >
+          Learn about how to create a personal access token
+        </a>
+      </p>
+      <h3>Personal Access Token(s)</h3>
+      {tokens.map(({ value, status, from }, i) => (
+        <div className="token" key={i}>
+          <div className="input">
+            <InputGroup
+              placeholder="token"
+              type="password"
+              value={value ?? ''}
+              onChange={(e) => handleChangeToken(i, e.target.value)}
+              onBlur={() => handleTestToken(i)}
+            />
+            {status === 'invalid' && <span className="error">Invalid</span>}
+            {status === 'valid' && <span className="success">Valid From: 
{from}</span>}
+          </div>
+          <Button minimal icon="cross" onClick={() => handleRemoveToken(i)} />
+        </div>
+      ))}
+      <div className="action">
+        <Button outlined small intent={Intent.PRIMARY} text="Another Token" 
icon="plus" onClick={handleCreateToken} />
+      </div>
+    </S.Wrapper>
+  );
+};
diff --git a/config-ui/src/pages/connection/form/styled.ts 
b/config-ui/src/pages/connection/form/components/github-token/styled.ts
similarity index 61%
copy from config-ui/src/pages/connection/form/styled.ts
copy to config-ui/src/pages/connection/form/components/github-token/styled.ts
index ba6584fe6..ffb01932d 100644
--- a/config-ui/src/pages/connection/form/styled.ts
+++ b/config-ui/src/pages/connection/form/components/github-token/styled.ts
@@ -20,55 +20,6 @@ import { Colors } from '@blueprintjs/core';
 import styled from 'styled-components';
 
 export const Wrapper = styled.div`
-  .bp4-form-group {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-
-    .bp4-label {
-      flex: 0 0 200px;
-      font-weight: 600;
-
-      .bp4-popover2-target {
-        display: inline;
-        margin: 0;
-        line-height: 1;
-        margin-left: 4px;
-      }
-
-      .bp4-text-muted {
-        color: ${Colors.RED3};
-      }
-    }
-
-    .bp4-form-content {
-      flex: auto;
-    }
-  }
-
-  .footer {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    margin-top: 32px;
-  }
-`;
-
-export const Label = styled.span`
-  display: inline-flex;
-  align-items: center;
-`;
-
-export const RateLimit = styled.div`
-  display: flex;
-  align-items: center;
-
-  & > .bp4-numeric-input {
-    margin-right: 8px;
-  }
-`;
-
-export const GitHubToken = styled.div`
   p {
     margin: 0 0 8px;
   }
@@ -84,5 +35,22 @@ export const GitHubToken = styled.div`
     align-items: center;
     justify-content: space-between;
     margin-bottom: 8px;
+
+    .input {
+      display: flex;
+      align-items: center;
+
+      & > span {
+        margin-left: 4px;
+
+        &.error {
+          color: ${Colors.RED3};
+        }
+
+        &.success {
+          color: ${Colors.GREEN3};
+        }
+      }
+    }
   }
 `;
diff --git a/config-ui/src/pages/pipeline/detail/index.ts 
b/config-ui/src/pages/connection/form/components/index.ts
similarity index 95%
copy from config-ui/src/pages/pipeline/detail/index.ts
copy to config-ui/src/pages/connection/form/components/index.ts
index 07d867746..d54071fb7 100644
--- a/config-ui/src/pages/pipeline/detail/index.ts
+++ b/config-ui/src/pages/connection/form/components/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './pipeline-detail';
+export * from './github-token';
diff --git a/config-ui/src/pages/connection/form/index.tsx 
b/config-ui/src/pages/connection/form/index.tsx
index 66c243c2a..0aa28c50f 100644
--- a/config-ui/src/pages/connection/form/index.tsx
+++ b/config-ui/src/pages/connection/form/index.tsx
@@ -18,7 +18,7 @@
 
 import React, { useState, useEffect, useMemo } from 'react';
 import { useParams, useHistory } from 'react-router-dom';
-import { omit, pick } from 'lodash';
+import { pick } from 'lodash';
 import {
   FormGroup,
   InputGroup,
@@ -36,13 +36,13 @@ import { PageHeader, Card, PageLoading } from 
'@/components';
 import type { PluginConfigConnectionType } from '@/plugins';
 import { Plugins, PluginConfig } from '@/plugins';
 
+import { GitHubToken } from './components';
 import { useForm } from './use-form';
 import * as S from './styled';
 
 export const ConnectionFormPage = () => {
   const [form, setForm] = useState<Record<string, any>>({});
   const [showRateLimit, setShowRateLimit] = useState(false);
-  const [githubTokens, setGitHubTokens] = useState<Record<string, string>>({});
 
   const history = useHistory();
   const { plugin, cid } = useParams<{ plugin: Plugins; cid?: string }>();
@@ -60,49 +60,14 @@ export const ConnectionFormPage = () => {
       ...(connection ?? {}),
     });
 
-    setGitHubTokens(
-      (connection?.token ?? '').split(',').reduce((acc: any, cur: string, 
index: number) => {
-        acc[index] = cur;
-        return acc;
-      }, {} as any),
-    );
-
     setShowRateLimit(connection?.rateLimitPerHour ? true : false);
   }, [initialValues, connection]);
 
-  useEffect(() => {
-    if (plugin === Plugins.GitHub) {
-      setForm({
-        ...form,
-        token: Object.values(githubTokens).filter(Boolean).join(','),
-      });
-    }
-  }, [githubTokens]);
-
   const error = useMemo(
     () => !!(fields.filter((field) => field.required) ?? []).find((field) => 
!form[field.key]),
     [form, fields],
   );
 
-  const handleChangeGitHubToken = (key: string, token: string) => {
-    setGitHubTokens({
-      ...githubTokens,
-      [key]: token,
-    });
-  };
-
-  const handleCreateToken = () => {
-    const keys = Object.keys(githubTokens);
-    setGitHubTokens({
-      ...githubTokens,
-      [+keys[keys.length - 1] + 1]: '',
-    });
-  };
-
-  const handleRemoveToken = (key: string) => {
-    setGitHubTokens(omit(githubTokens, [key]));
-  };
-
   const handleTest = () =>
     onTest(pick(form, ['endpoint', 'token', 'username', 'password', 'app_id', 
'secret_key', 'proxy']));
 
@@ -182,43 +147,16 @@ export const ConnectionFormPage = () => {
           />
         )}
         {type === 'github-token' && (
-          <S.GitHubToken>
-            <p>
-              Add one or more personal token(s) for authentication from you 
and your organization members. Multiple
-              tokens can help speed up the data collection process.{' '}
-            </p>
-            <p>
-              <a
-                
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token";
-                target="_blank"
-                rel="noreferrer"
-              >
-                Learn about how to create a personal access token
-              </a>
-            </p>
-            <h3>Personal Access Token(s)</h3>
-            {Object.entries(githubTokens).map(([key, value]) => (
-              <div className="token" key={key}>
-                <InputGroup
-                  placeholder="token"
-                  type="password"
-                  value={value}
-                  onChange={(e) => handleChangeGitHubToken(key, 
e.target.value)}
-                />
-                <Button minimal icon="cross" onClick={() => 
handleRemoveToken(key)} />
-              </div>
-            ))}
-            <div className="action">
-              <Button
-                outlined
-                small
-                intent={Intent.PRIMARY}
-                text="Another Token"
-                icon="plus"
-                onClick={handleCreateToken}
-              />
-            </div>
-          </S.GitHubToken>
+          <GitHubToken
+            form={form}
+            value={form.token}
+            onChange={(value) =>
+              setForm({
+                ...form,
+                token: value,
+              })
+            }
+          />
         )}
       </FormGroup>
     );
diff --git a/config-ui/src/pages/connection/form/styled.ts 
b/config-ui/src/pages/connection/form/styled.ts
index ba6584fe6..564bc0961 100644
--- a/config-ui/src/pages/connection/form/styled.ts
+++ b/config-ui/src/pages/connection/form/styled.ts
@@ -67,22 +67,3 @@ export const RateLimit = styled.div`
     margin-right: 8px;
   }
 `;
-
-export const GitHubToken = styled.div`
-  p {
-    margin: 0 0 8px;
-  }
-
-  h3 {
-    margin: 0 0 8px;
-    padding: 0;
-    font-size: 14px;
-  }
-
-  .token {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    margin-bottom: 8px;
-  }
-`;
diff --git 
a/config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts 
b/config-ui/src/pages/pipeline/api.ts
similarity index 91%
copy from config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts
copy to config-ui/src/pages/pipeline/api.ts
index 6d038c6a8..fc0fdaa03 100644
--- a/config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts
+++ b/config-ui/src/pages/pipeline/api.ts
@@ -18,4 +18,6 @@
 
 import { request } from '@/utils';
 
+export const getPipelineHistorical = (id: ID) => 
request(`/blueprints/${id}/pipelines`);
+
 export const getPipelineLog = (id: ID) => 
request(`/pipelines/${id}/logging.tar.gz`);
diff --git 
a/config-ui/src/pages/blueprint/detail/components/pipeline-list/index.tsx 
b/config-ui/src/pages/pipeline/components/historical/index.tsx
similarity index 74%
rename from 
config-ui/src/pages/blueprint/detail/components/pipeline-list/index.tsx
rename to config-ui/src/pages/pipeline/components/historical/index.tsx
index 15637060f..b5df1e76e 100644
--- a/config-ui/src/pages/blueprint/detail/components/pipeline-list/index.tsx
+++ b/config-ui/src/pages/pipeline/components/historical/index.tsx
@@ -17,53 +17,46 @@
  */
 
 import React, { useState, useMemo } from 'react';
-import { ButtonGroup, Button, Icon, Intent, Colors, Position, IconName } from 
'@blueprintjs/core';
+import { Icon, ButtonGroup, Button, Position, Intent, IconName } from 
'@blueprintjs/core';
 import { Tooltip2 } from '@blueprintjs/popover2';
 import { pick } from 'lodash';
 import { saveAs } from 'file-saver';
-import styled from 'styled-components';
 
 import { DEVLAKE_ENDPOINT } from '@/config';
-import { Card, Table, ColumnType, Loading, Inspector } from '@/components';
-import { PipelineType, StatusEnum, STATUS_ICON, STATUS_LABEL, STATUS_CLS } 
from '@/pages';
+import { Card, Loading, Table, ColumnType, Inspector } from '@/components';
+import { useAutoRefresh } from '@/hooks';
 import { formatTime, duration } from '@/utils';
 
-import * as API from './api';
+import type { PipelineType } from '../../types';
+import { StatusEnum } from '../../types';
+import { STATUS_ICON, STATUS_LABEL, STATUS_CLS } from '../../misc';
+import * as API from '../../api';
 
-const StatusColumn = styled.div`
-  display: flex;
-  align-items: center;
-
-  .bp4-icon {
-    margin-right: 4px;
-  }
-
-  &.ready,
-  &.cancel {
-    color: #94959f;
-  }
-
-  &.loading {
-    color: #7497f7;
-  }
-
-  &.success {
-    color: ${Colors.GREEN3};
-  }
-
-  &.error {
-    color: ${Colors.RED3};
-  }
-`;
+import * as S from './styled';
 
 interface Props {
-  pipelines: PipelineType[];
+  blueprintId: ID;
 }
 
-export const PipelineList = ({ pipelines }: Props) => {
+export const PipelineHistorical = ({ blueprintId }: Props) => {
   const [isOpen, setIsOpen] = useState(false);
   const [json, setJson] = useState<any>({});
 
+  const { loading, data } = useAutoRefresh<PipelineType[]>(
+    async () => {
+      const res = await API.getPipelineHistorical(blueprintId);
+      return res.pipelines;
+    },
+    [],
+    {
+      cancel: (data) =>
+        !!(
+          data &&
+          data.every((it) => [StatusEnum.COMPLETED, StatusEnum.CANCELLED, 
StatusEnum.FAILED].includes(it.status))
+        ),
+    },
+  );
+
   const handleDownloadLog = async (id: ID) => {
     const res = await API.getPipelineLog(id);
     if (res) {
@@ -79,14 +72,14 @@ export const PipelineList = ({ pipelines }: Props) => {
           dataIndex: 'status',
           key: 'status',
           render: (val: StatusEnum) => (
-            <StatusColumn className={STATUS_CLS(val)}>
+            <S.StatusColumn className={STATUS_CLS(val)}>
               {STATUS_ICON[val] === 'loading' ? (
                 <Loading style={{ marginRight: 4 }} size={14} />
               ) : (
                 <Icon style={{ marginRight: 4 }} icon={STATUS_ICON[val] as 
IconName} />
               )}
               <span>{STATUS_LABEL[val]}</span>
-            </StatusColumn>
+            </S.StatusColumn>
           ),
         },
         {
@@ -135,11 +128,21 @@ export const PipelineList = ({ pipelines }: Props) => {
     [],
   );
 
-  return !pipelines.length ? (
-    <Card>There are no historical runs associated with this blueprint.</Card>
-  ) : (
+  if (loading) {
+    return (
+      <Card>
+        <Loading />
+      </Card>
+    );
+  }
+
+  if (!data) {
+    return <Card>There are no historical runs associated with this 
blueprint.</Card>;
+  }
+
+  return (
     <div>
-      <Table columns={columns} dataSource={pipelines} />
+      <Table columns={columns} dataSource={data} />
       <Inspector isOpen={isOpen} title={`Pipeline ${json?.id}`} data={json} 
onClose={() => setIsOpen(false)} />
     </div>
   );
diff --git 
a/config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts 
b/config-ui/src/pages/pipeline/components/historical/styled.ts
similarity index 67%
rename from config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts
rename to config-ui/src/pages/pipeline/components/historical/styled.ts
index 6d038c6a8..21362c88b 100644
--- a/config-ui/src/pages/blueprint/detail/components/pipeline-list/api.ts
+++ b/config-ui/src/pages/pipeline/components/historical/styled.ts
@@ -16,6 +16,31 @@
  *
  */
 
-import { request } from '@/utils';
+import { Colors } from '@blueprintjs/core';
+import styled from 'styled-components';
 
-export const getPipelineLog = (id: ID) => 
request(`/pipelines/${id}/logging.tar.gz`);
+export const StatusColumn = styled.div`
+  display: flex;
+  align-items: center;
+
+  .bp4-icon {
+    margin-right: 4px;
+  }
+
+  &.ready,
+  &.cancel {
+    color: #94959f;
+  }
+
+  &.loading {
+    color: #7497f7;
+  }
+
+  &.success {
+    color: ${Colors.GREEN3};
+  }
+
+  &.error {
+    color: ${Colors.RED3};
+  }
+`;
diff --git a/config-ui/src/pages/pipeline/detail/index.ts 
b/config-ui/src/pages/pipeline/components/index.ts
similarity index 95%
copy from config-ui/src/pages/pipeline/detail/index.ts
copy to config-ui/src/pages/pipeline/components/index.ts
index 07d867746..484dfb785 100644
--- a/config-ui/src/pages/pipeline/detail/index.ts
+++ b/config-ui/src/pages/pipeline/components/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './pipeline-detail';
+export * from './historical';
diff --git a/config-ui/src/pages/pipeline/detail/api.ts 
b/config-ui/src/pages/pipeline/detail/api.ts
index 99cfbd75b..2b4a9bc25 100644
--- a/config-ui/src/pages/pipeline/detail/api.ts
+++ b/config-ui/src/pages/pipeline/detail/api.ts
@@ -27,8 +27,8 @@ export const deletePipeline = (id: ID) =>
     method: 'delete',
   });
 
-export const pipeLineRerun = (id: ID) =>
-  request(`/pipeline/${id}/rerun`, {
+export const pipelineRerun = (id: ID) =>
+  request(`/pipelines/${id}/rerun`, {
     method: 'post',
   });
 
diff --git a/config-ui/src/pages/pipeline/detail/index.ts 
b/config-ui/src/pages/pipeline/detail/index.ts
index 07d867746..d0223bac2 100644
--- a/config-ui/src/pages/pipeline/detail/index.ts
+++ b/config-ui/src/pages/pipeline/detail/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './pipeline-detail';
+export * from './pipeline-info';
diff --git a/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx 
b/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx
index 260437ab9..c280dd99a 100644
--- a/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx
+++ b/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx
@@ -16,40 +16,64 @@
  *
  */
 
-import React, { useState, useMemo } from 'react';
-import { Icon, Button, Collapse, IconName, Intent } from '@blueprintjs/core';
+import React, { useState } from 'react';
+import { Icon, Button, Collapse, Intent, IconName } from '@blueprintjs/core';
 import { groupBy } from 'lodash';
 import classNames from 'classnames';
 
 import { Card, Loading } from '@/components';
+import { useAutoRefresh } from '@/hooks';
 import { formatTime, duration } from '@/utils';
 
+import type { PipelineType, TaskType } from '../types';
 import { StatusEnum } from '../types';
 import { STATUS_ICON, STATUS_LABEL, STATUS_CLS } from '../misc';
 
-import type { UseDetailProps } from './use-detail';
 import { useDetail } from './use-detail';
 import { Task } from './components';
+import * as API from './api';
 import * as S from './styled';
 
-interface Props extends UseDetailProps {}
+interface Props {
+  id: ID;
+}
 
-export const PipelineDetail = ({ ...props }: Props) => {
+export const PipelineDetail = ({ id }: Props) => {
   const [isOpen, setIsOpen] = useState(true);
 
-  const { loading, operating, pipeline, tasks, onCancel, onRerun, onRerunTask 
} = useDetail({ ...props });
-
-  const stages = useMemo(() => groupBy(tasks, 'pipelineRow'), [tasks]);
+  const { version, operating, onCancel, onRerun, onRerunTask } = useDetail({ 
id });
+
+  const { loading, data } = useAutoRefresh<{
+    pipeline: PipelineType;
+    tasks: TaskType[];
+  }>(
+    async () => {
+      const [pipeRes, taskRes] = await Promise.all([API.getPipeline(id), 
API.getPipelineTasks(id)]);
+
+      return {
+        pipeline: pipeRes,
+        tasks: taskRes.tasks,
+      };
+    },
+    [version],
+    {
+      cancel: (data) => {
+        const { pipeline } = data ?? {};
+        return !!(
+          pipeline && [StatusEnum.COMPLETED, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(pipeline.status)
+        );
+      },
+    },
+  );
 
-  if (loading) {
+  if (loading || !data) {
     return <Loading />;
   }
 
-  if (!pipeline) {
-    return <Card>There is no current run for this blueprint.</Card>;
-  }
+  const { pipeline, tasks } = data;
 
   const { status, beganAt, finishedAt, stage, finishedTasks, totalTasks, 
message } = pipeline;
+  const stages = groupBy(tasks, 'pipelineRow');
 
   const handleToggleOpen = () => {
     setIsOpen(!isOpen);
diff --git a/config-ui/src/pages/blueprint/detail/types.ts 
b/config-ui/src/pages/pipeline/detail/pipeline-info.tsx
similarity index 71%
copy from config-ui/src/pages/blueprint/detail/types.ts
copy to config-ui/src/pages/pipeline/detail/pipeline-info.tsx
index 174e43525..13e4ced79 100644
--- a/config-ui/src/pages/blueprint/detail/types.ts
+++ b/config-ui/src/pages/pipeline/detail/pipeline-info.tsx
@@ -16,13 +16,20 @@
  *
  */
 
-import { Plugins } from '@/plugins';
+import React from 'react';
 
-export type ConfigConnectionItemType = {
-  icon: string;
-  name: string;
-  connectionId: ID;
-  plugin: Plugins;
-  entities: string[];
-  scopeIds: ID[];
+import { Card } from '@/components';
+
+import { PipelineDetail } from './pipeline-detail';
+
+interface Props {
+  id?: ID;
+}
+
+export const PipelineInfo = ({ id }: Props) => {
+  if (!id) {
+    return <Card>There is no current run for this blueprint.</Card>;
+  }
+
+  return <PipelineDetail id={id} />;
 };
diff --git a/config-ui/src/pages/pipeline/detail/use-detail.ts 
b/config-ui/src/pages/pipeline/detail/use-detail.ts
index 96b16ce95..00f7eb582 100644
--- a/config-ui/src/pages/pipeline/detail/use-detail.ts
+++ b/config-ui/src/pages/pipeline/detail/use-detail.ts
@@ -16,106 +16,58 @@
  *
  */
 
-import { useState, useEffect, useMemo, useRef } from 'react';
+import { useState, useMemo } from 'react';
 
 import { operator } from '@/utils';
 
-import type { PipelineType, TaskType } from '../types';
-import { StatusEnum } from '../types';
-
 import * as API from './api';
 
-const pollTimer = 5000;
-
-export interface UseDetailProps {
-  id?: ID;
+export interface Props {
+  id: ID;
 }
 
-export const useDetail = ({ id }: UseDetailProps) => {
+export const useDetail = ({ id }: Props) => {
   const [version, setVersion] = useState(0);
-  const [loading, setLoading] = useState(false);
   const [operating, setOperating] = useState(false);
-  const [pipeline, setPipeline] = useState<PipelineType>();
-  const [tasks, setTasks] = useState<TaskType[]>([]);
-
-  const timer = useRef<any>();
-
-  const getPipeline = async () => {
-    if (!id) return;
-    setLoading(true);
-    try {
-      const [pipeRes, taskRes] = await Promise.all([API.getPipeline(id), 
API.getPipelineTasks(id)]);
-
-      setPipeline(pipeRes);
-      setTasks(taskRes.tasks);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    getPipeline();
-  }, []);
-
-  useEffect(() => {
-    timer.current = setInterval(() => {
-      getPipeline();
-    }, pollTimer);
-    return () => clearInterval(timer.current);
-  }, [version]);
-
-  useEffect(() => {
-    if (pipeline && [StatusEnum.COMPLETED, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(pipeline.status)) {
-      clearInterval(timer.current);
-    }
-  }, [pipeline]);
 
   const handlePipelineCancel = async () => {
-    if (!id) return;
     const [success] = await operator(() => API.deletePipeline(id), {
       setOperating,
     });
 
     if (success) {
-      getPipeline();
       setVersion(version + 1);
     }
   };
 
   const handlePipelineRerun = async () => {
-    if (!id) return;
-    const [success] = await operator(() => API.pipeLineRerun(id), {
+    const [success] = await operator(() => API.pipelineRerun(id), {
       setOperating,
     });
 
     if (success) {
-      getPipeline();
       setVersion(version + 1);
     }
   };
 
   const handleTaskRertun = async (id: ID) => {
-    if (!id) return;
     const [success] = await operator(() => API.taskRerun(id), {
       setOperating,
     });
 
     if (success) {
-      getPipeline();
       setVersion(version + 1);
     }
   };
 
   return useMemo(
     () => ({
-      loading,
+      version,
       operating,
-      pipeline,
-      tasks,
       onCancel: handlePipelineCancel,
       onRerun: handlePipelineRerun,
       onRerunTask: handleTaskRertun,
     }),
-    [loading, operating, pipeline, tasks],
+    [version, operating],
   );
 };
diff --git a/config-ui/src/pages/pipeline/index.ts 
b/config-ui/src/pages/pipeline/index.ts
index fb5ee63e4..5c45114f7 100644
--- a/config-ui/src/pages/pipeline/index.ts
+++ b/config-ui/src/pages/pipeline/index.ts
@@ -18,4 +18,5 @@
 
 export * from './types';
 export * from './misc';
+export * from './components';
 export * from './detail';
diff --git a/config-ui/src/pages/project/detail/use-project.ts 
b/config-ui/src/pages/project/detail/use-project.ts
index d95a7cd17..6332a4f9c 100644
--- a/config-ui/src/pages/project/detail/use-project.ts
+++ b/config-ui/src/pages/project/detail/use-project.ts
@@ -131,6 +131,6 @@ export const useProject = (name: string) => {
       onSelectWebhook: handleSelectWebhook,
       onCreateWebhook: handleCreateWebhook,
     }),
-    [loading, project, saving],
+    [loading, project, saving, name],
   );
 };
diff --git a/config-ui/src/plugins/common/data-scope/use-data-scope.ts 
b/config-ui/src/plugins/common/data-scope/use-data-scope.ts
index 773ac968a..6f1b2033b 100644
--- a/config-ui/src/plugins/common/data-scope/use-data-scope.ts
+++ b/config-ui/src/plugins/common/data-scope/use-data-scope.ts
@@ -28,7 +28,10 @@ export interface UseDataScope {
   plugin: string;
   connectionId: ID;
   entities: string[];
-  initialValues?: any;
+  initialValues?: {
+    scope?: any;
+    entites?: string[];
+  };
   onSave?: (scope: any) => void;
 }
 
@@ -38,12 +41,12 @@ export const useDataScope = ({ plugin, connectionId, 
entities, initialValues, on
   const [selectedEntities, setSelectedEntities] = useState<string[]>([]);
 
   useEffect(() => {
-    setSelectedScope(initialValues ?? []);
-  }, [initialValues]);
+    setSelectedScope(initialValues?.scope ?? []);
+  }, [initialValues?.scope]);
 
   useEffect(() => {
-    setSelectedEntities(entities ?? []);
-  }, [entities]);
+    setSelectedEntities(initialValues?.entites ?? entities);
+  }, [entities, initialValues?.entites]);
 
   const getPluginId = (scope: any) => {
     switch (true) {
diff --git a/config-ui/src/plugins/gitlab/components/miller-columns/index.tsx 
b/config-ui/src/plugins/gitlab/components/miller-columns/index.tsx
index a3fd05cb6..d98ee3234 100644
--- a/config-ui/src/plugins/gitlab/components/miller-columns/index.tsx
+++ b/config-ui/src/plugins/gitlab/components/miller-columns/index.tsx
@@ -76,7 +76,7 @@ export const MillerColumns = ({ connectionId, disabledItems, 
selectedItems, onCh
 
   return (
     <MillerColumnsSelect
-      columnCount={3}
+      columnCount={2.5}
       columnHeight={300}
       getCanExpand={(it) => it.type === 'group'}
       getHasMore={getHasMore}
diff --git a/config-ui/src/plugins/jenkins/components/miller-columns/index.tsx 
b/config-ui/src/plugins/jenkins/components/miller-columns/index.tsx
index 7b89e26ca..a2a3ddee0 100644
--- a/config-ui/src/plugins/jenkins/components/miller-columns/index.tsx
+++ b/config-ui/src/plugins/jenkins/components/miller-columns/index.tsx
@@ -66,7 +66,7 @@ export const MillerColumns = ({ connectionId, selectedItems, 
onChangeItems }: Pr
 
   return (
     <MillerColumnsSelect
-      columnCount={2}
+      columnCount={2.5}
       columnHeight={300}
       getCanExpand={(it) => it.type === 'folder'}
       getHasMore={getHasMore}

Reply via email to