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 94b96f838 refactor(config-ui): the pipeline (#4196)
94b96f838 is described below

commit 94b96f838a9779034e69be2e5407404d79611f4e
Author: 青湛 <[email protected]>
AuthorDate: Mon Jan 16 17:51:41 2023 +0800

    refactor(config-ui): the pipeline (#4196)
    
    * feat(config-ui): update global styles
    
    * refactor(config-ui): move components/delete-button to 
components/action/delete-button
    
    * feat(config-ui): add new action component icon-button
    
    * feat(config-ui): add new component text-tooltip
    
    * feat(config-ui): add new hook use-operator
    
    * feat(config-ui): add pipeline component context
    
    * feat(config-ui): add pipeline component cancel and rerun
    
    * refactor(config-ui): use pipeline status component to replace misc
    
    * refactor(config-ui): move pipeline/detail/components/task to 
pipeline/components/task
    
    * refactor(config-ui): remove the pages/pipeline/detail
    
    * feat(config-ui): add pipeline component info and tasks
    
    * refactor(config-ui): optimize pipeline historical with new components
    
    * refactor(config-ui): optimize blueprint detail status with new components
    
    * fix(config-ui): optimize run now failure prompt
---
 .../{ => action}/delete-button/index.tsx           |   0
 .../action/icon-button/index.tsx}                  |  23 +--
 .../pipeline/detail => components/action}/index.ts |   3 +-
 config-ui/src/components/index.ts                  |   3 +-
 .../text-tooltip/index.tsx}                        |  47 ++---
 config-ui/src/hooks/index.ts                       |   1 +
 .../detail/use-detail.ts => hooks/use-operator.ts} |  51 ++----
 config-ui/src/index.css                            |  36 ++++
 .../src/pages/blueprint/detail/panel/status.tsx    |  30 +++-
 config-ui/src/pages/blueprint/detail/use-detail.ts |   1 +
 config-ui/src/pages/pipeline/api.ts                |  19 ++
 .../cancel/index.tsx}                              |  25 ++-
 .../api.ts => components/context/index.tsx}        |  32 ++--
 .../pages/pipeline/components/historical/index.tsx |  68 ++++---
 config-ui/src/pages/pipeline/components/index.ts   |   4 +-
 .../src/pages/pipeline/components/info/index.tsx   |  97 ++++++++++
 .../components/{historical => info}/styled.ts      |  44 +++--
 .../src/pages/pipeline/components/rerun/index.tsx  |  51 ++++++
 .../{misc.ts => components/status/index.tsx}       |  28 ++-
 .../components/{historical => status}/styled.ts    |   9 +-
 .../{detail => }/components/task/index.tsx         |  54 +++---
 .../{detail => }/components/task/styled.ts         |   9 +-
 .../src/pages/pipeline/components/tasks/index.tsx  | 125 +++++++++++++
 .../{detail => components/tasks}/styled.ts         |  68 +------
 .../src/pages/pipeline/detail/components/index.ts  |  19 --
 .../src/pages/pipeline/detail/pipeline-detail.tsx  | 195 ---------------------
 config-ui/src/pages/pipeline/index.ts              |   2 -
 27 files changed, 565 insertions(+), 479 deletions(-)

diff --git a/config-ui/src/components/delete-button/index.tsx 
b/config-ui/src/components/action/delete-button/index.tsx
similarity index 100%
rename from config-ui/src/components/delete-button/index.tsx
rename to config-ui/src/components/action/delete-button/index.tsx
diff --git a/config-ui/src/pages/pipeline/detail/pipeline-info.tsx 
b/config-ui/src/components/action/icon-button/index.tsx
similarity index 63%
copy from config-ui/src/pages/pipeline/detail/pipeline-info.tsx
copy to config-ui/src/components/action/icon-button/index.tsx
index 13e4ced79..7a40dc80d 100644
--- a/config-ui/src/pages/pipeline/detail/pipeline-info.tsx
+++ b/config-ui/src/components/action/icon-button/index.tsx
@@ -17,19 +17,20 @@
  */
 
 import React from 'react';
-
-import { Card } from '@/components';
-
-import { PipelineDetail } from './pipeline-detail';
+import { Button, Intent, Position, IconName } from '@blueprintjs/core';
+import { Tooltip2 } from '@blueprintjs/popover2';
 
 interface Props {
-  id?: ID;
+  icon: IconName;
+  tooltip: string;
+  loading?: boolean;
+  onClick?: () => void;
 }
 
-export const PipelineInfo = ({ id }: Props) => {
-  if (!id) {
-    return <Card>There is no current run for this blueprint.</Card>;
-  }
-
-  return <PipelineDetail id={id} />;
+export const IconButton = ({ icon, tooltip, loading, onClick }: Props) => {
+  return (
+    <Tooltip2 intent={Intent.PRIMARY} position={Position.TOP} 
content={tooltip}>
+      <Button loading={loading} minimal intent={Intent.PRIMARY} icon={icon} 
onClick={onClick} />
+    </Tooltip2>
+  );
 };
diff --git a/config-ui/src/pages/pipeline/detail/index.ts 
b/config-ui/src/components/action/index.ts
similarity index 92%
rename from config-ui/src/pages/pipeline/detail/index.ts
rename to config-ui/src/components/action/index.ts
index d0223bac2..06dc0b92e 100644
--- a/config-ui/src/pages/pipeline/detail/index.ts
+++ b/config-ui/src/components/action/index.ts
@@ -16,4 +16,5 @@
  *
  */
 
-export * from './pipeline-info';
+export * from './delete-button';
+export * from './icon-button';
diff --git a/config-ui/src/components/index.ts 
b/config-ui/src/components/index.ts
index 50248a71d..d46332c87 100644
--- a/config-ui/src/components/index.ts
+++ b/config-ui/src/components/index.ts
@@ -26,4 +26,5 @@ export * from './toast';
 export * from './logo';
 export * from './card';
 export * from './inspector';
-export * from './delete-button';
+export * from './action';
+export * from './text-tooltip';
diff --git a/config-ui/src/pages/pipeline/components/historical/styled.ts 
b/config-ui/src/components/text-tooltip/index.tsx
similarity index 56%
copy from config-ui/src/pages/pipeline/components/historical/styled.ts
copy to config-ui/src/components/text-tooltip/index.tsx
index 21362c88b..03d90e766 100644
--- a/config-ui/src/pages/pipeline/components/historical/styled.ts
+++ b/config-ui/src/components/text-tooltip/index.tsx
@@ -16,31 +16,34 @@
  *
  */
 
-import { Colors } from '@blueprintjs/core';
+import React from 'react';
+import type { IntentProps } from '@blueprintjs/core';
+import { Position } from '@blueprintjs/core';
+import { Tooltip2 } from '@blueprintjs/popover2';
 import styled from 'styled-components';
 
-export const StatusColumn = styled.div`
-  display: flex;
-  align-items: center;
+const Wrapper = styled.div`
+  width: 100%;
 
-  .bp4-icon {
-    margin-right: 4px;
-  }
-
-  &.ready,
-  &.cancel {
-    color: #94959f;
-  }
-
-  &.loading {
-    color: #7497f7;
+  & > .bp4-popover2-target {
+    display: block;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
   }
+`;
 
-  &.success {
-    color: ${Colors.GREEN3};
-  }
+interface Props extends IntentProps {
+  content: string;
+  children: React.ReactNode;
+}
 
-  &.error {
-    color: ${Colors.RED3};
-  }
-`;
+export const TextTooltip = ({ intent, content, children }: Props) => {
+  return (
+    <Wrapper>
+      <Tooltip2 intent={intent} position={Position.TOP} content={content}>
+        {children}
+      </Tooltip2>
+    </Wrapper>
+  );
+};
diff --git a/config-ui/src/hooks/index.ts b/config-ui/src/hooks/index.ts
index bd3a94b28..bdf49dea4 100644
--- a/config-ui/src/hooks/index.ts
+++ b/config-ui/src/hooks/index.ts
@@ -17,3 +17,4 @@
  */
 
 export * from './use-auto-refresh';
+export * from './use-operator';
diff --git a/config-ui/src/pages/pipeline/detail/use-detail.ts 
b/config-ui/src/hooks/use-operator.ts
similarity index 55%
rename from config-ui/src/pages/pipeline/detail/use-detail.ts
rename to config-ui/src/hooks/use-operator.ts
index 00f7eb582..1dba1f79c 100644
--- a/config-ui/src/pages/pipeline/detail/use-detail.ts
+++ b/config-ui/src/hooks/use-operator.ts
@@ -20,54 +20,33 @@ import { useState, useMemo } from 'react';
 
 import { operator } from '@/utils';
 
-import * as API from './api';
-
-export interface Props {
-  id: ID;
-}
-
-export const useDetail = ({ id }: Props) => {
-  const [version, setVersion] = useState(0);
+export const useOperator = <T>(
+  request: (paylod?: any) => Promise<T>,
+  options?: {
+    callback?: () => void;
+    formatReason?: () => string;
+    formatMessage?: () => string;
+  },
+) => {
   const [operating, setOperating] = useState(false);
 
-  const handlePipelineCancel = async () => {
-    const [success] = await operator(() => API.deletePipeline(id), {
-      setOperating,
-    });
-
-    if (success) {
-      setVersion(version + 1);
-    }
-  };
-
-  const handlePipelineRerun = async () => {
-    const [success] = await operator(() => API.pipelineRerun(id), {
-      setOperating,
-    });
-
-    if (success) {
-      setVersion(version + 1);
-    }
-  };
-
-  const handleTaskRertun = async (id: ID) => {
-    const [success] = await operator(() => API.taskRerun(id), {
+  const handleSubmit = async (paylod?: any) => {
+    const [success] = await operator(() => request(paylod), {
       setOperating,
+      formatReason: options?.formatReason,
+      formatMessage: options?.formatMessage,
     });
 
     if (success) {
-      setVersion(version + 1);
+      options?.callback?.();
     }
   };
 
   return useMemo(
     () => ({
-      version,
       operating,
-      onCancel: handlePipelineCancel,
-      onRerun: handlePipelineRerun,
-      onRerunTask: handleTaskRertun,
+      onSubmit: handleSubmit,
     }),
-    [version, operating],
+    [operating],
   );
 };
diff --git a/config-ui/src/index.css b/config-ui/src/index.css
index fb1a71a35..352b633b2 100644
--- a/config-ui/src/index.css
+++ b/config-ui/src/index.css
@@ -22,3 +22,39 @@
 @import '~@blueprintjs/select/lib/css/blueprint-select.css';
 @import '~@blueprintjs/popover2/lib/css/blueprint-popover2.css';
 @import '~@blueprintjs/datetime/lib/css/blueprint-datetime.css';
+
+h1 {
+  font-size: 20px;
+}
+
+h2 {
+  font-size: 18px;
+}
+
+h3 {
+  font-size: 16px;
+}
+
+h4 {
+  font-size: 14px;
+}
+
+h5 {
+  font-size: 12px;
+}
+
+h6 {
+  font-size: 10px;
+}
+
+ul {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+p {
+  margin: 0 0 8px 0;
+  font-size: 12px;
+  color: #94959f;
+}
diff --git a/config-ui/src/pages/blueprint/detail/panel/status.tsx 
b/config-ui/src/pages/blueprint/detail/panel/status.tsx
index b3d10dc28..cb01d53ea 100644
--- a/config-ui/src/pages/blueprint/detail/panel/status.tsx
+++ b/config-ui/src/pages/blueprint/detail/panel/status.tsx
@@ -20,7 +20,8 @@ import React, { useMemo } from 'react';
 import { Button, Intent } from '@blueprintjs/core';
 
 import { getCron } from '@/config';
-import { PipelineInfo, PipelineHistorical } from '@/pages';
+import { Card } from '@/components';
+import { PipelineContextProvider, PipelineInfo, PipelineTasks, 
PipelineHistorical } from '@/pages';
 import { formatTime } from '@/utils';
 
 import type { BlueprintType } from '../../types';
@@ -54,14 +55,25 @@ export const Status = ({ blueprint, pipelineId, operating, 
onRun }: Props) => {
           />
         </span>
       </div>
-      <div className="block">
-        <h3>Current Pipeline</h3>
-        <PipelineInfo id={pipelineId} />
-      </div>
-      <div className="block">
-        <h3>Historical Pipelines</h3>
-        <PipelineHistorical blueprintId={blueprint.id} />
-      </div>
+      <PipelineContextProvider>
+        <div className="block">
+          <h3>Current Pipeline</h3>
+          {!pipelineId ? (
+            <Card>There is no current run for this blueprint.</Card>
+          ) : (
+            <>
+              <PipelineInfo id={pipelineId} />
+              <Card style={{ marginTop: 16 }}>
+                <PipelineTasks id={pipelineId} />
+              </Card>
+            </>
+          )}
+        </div>
+        <div className="block">
+          <h3>Historical Pipelines</h3>
+          <PipelineHistorical blueprintId={blueprint.id} />
+        </div>
+      </PipelineContextProvider>
     </S.StatusPanel>
   );
 };
diff --git a/config-ui/src/pages/blueprint/detail/use-detail.ts 
b/config-ui/src/pages/blueprint/detail/use-detail.ts
index dda5c7aca..344ffc522 100644
--- a/config-ui/src/pages/blueprint/detail/use-detail.ts
+++ b/config-ui/src/pages/blueprint/detail/use-detail.ts
@@ -61,6 +61,7 @@ export const useDetail = ({ id }: UseDetailProps) => {
   const handleRun = async () => {
     const [success] = await operator(() => API.runBlueprint(id), {
       setOperating,
+      formatReason: (err) => (err as any).response?.data?.message,
     });
 
     if (success) {
diff --git a/config-ui/src/pages/pipeline/api.ts 
b/config-ui/src/pages/pipeline/api.ts
index fc0fdaa03..f00fbf623 100644
--- a/config-ui/src/pages/pipeline/api.ts
+++ b/config-ui/src/pages/pipeline/api.ts
@@ -18,6 +18,25 @@
 
 import { request } from '@/utils';
 
+export const getPipeline = (id: ID) => request(`/pipelines/${id}`);
+
+export const getPipelineTasks = (id: ID) => request(`/pipelines/${id}/tasks`);
+
+export const deletePipeline = (id: ID) =>
+  request(`/pipelines/${id}`, {
+    method: 'delete',
+  });
+
 export const getPipelineHistorical = (id: ID) => 
request(`/blueprints/${id}/pipelines`);
 
+export const pipelineRerun = (id: ID) =>
+  request(`/pipelines/${id}/rerun`, {
+    method: 'post',
+  });
+
+export const taskRerun = (id: ID) =>
+  request(`/tasks/${id}/rerun`, {
+    method: 'post',
+  });
+
 export const getPipelineLog = (id: ID) => 
request(`/pipelines/${id}/logging.tar.gz`);
diff --git a/config-ui/src/pages/pipeline/detail/pipeline-info.tsx 
b/config-ui/src/pages/pipeline/components/cancel/index.tsx
similarity index 56%
rename from config-ui/src/pages/pipeline/detail/pipeline-info.tsx
rename to config-ui/src/pages/pipeline/components/cancel/index.tsx
index 13e4ced79..aae3a30d2 100644
--- a/config-ui/src/pages/pipeline/detail/pipeline-info.tsx
+++ b/config-ui/src/pages/pipeline/components/cancel/index.tsx
@@ -18,18 +18,29 @@
 
 import React from 'react';
 
-import { Card } from '@/components';
+import { IconButton } from '@/components';
+import { useOperator } from '@/hooks';
 
-import { PipelineDetail } from './pipeline-detail';
+import { StatusEnum } from '../../types';
+import * as API from '../../api';
+
+import { usePipeline } from '../context';
 
 interface Props {
-  id?: ID;
+  id: ID;
+  status: StatusEnum;
 }
 
-export const PipelineInfo = ({ id }: Props) => {
-  if (!id) {
-    return <Card>There is no current run for this blueprint.</Card>;
+export const PipelineCancel = ({ id, status }: Props) => {
+  const { setVersion } = usePipeline();
+
+  const { operating, onSubmit } = useOperator(() => API.deletePipeline(id), {
+    callback: () => setVersion((v) => v + 1),
+  });
+
+  if (![StatusEnum.ACTIVE, StatusEnum.RUNNING, 
StatusEnum.RERUN].includes(status)) {
+    return null;
   }
 
-  return <PipelineDetail id={id} />;
+  return <IconButton loading={operating} icon="disable" tooltip="Cancel" 
onClick={onSubmit} />;
 };
diff --git a/config-ui/src/pages/pipeline/detail/api.ts 
b/config-ui/src/pages/pipeline/components/context/index.tsx
similarity index 59%
rename from config-ui/src/pages/pipeline/detail/api.ts
rename to config-ui/src/pages/pipeline/components/context/index.tsx
index 2b4a9bc25..77b883559 100644
--- a/config-ui/src/pages/pipeline/detail/api.ts
+++ b/config-ui/src/pages/pipeline/components/context/index.tsx
@@ -16,23 +16,23 @@
  *
  */
 
-import { request } from '@/utils';
+import React, { useContext, useState } from 'react';
 
-export const getPipeline = (id: ID) => request(`/pipelines/${id}`);
+const PipelineContext = React.createContext<{
+  version: number;
+  setVersion: React.Dispatch<React.SetStateAction<number>>;
+}>({
+  version: 0,
+  setVersion: () => {},
+});
 
-export const getPipelineTasks = (id: ID) => request(`/pipelines/${id}/tasks`);
+interface Props {
+  children: React.ReactNode;
+}
 
-export const deletePipeline = (id: ID) =>
-  request(`/pipelines/${id}`, {
-    method: 'delete',
-  });
+export const PipelineContextProvider = ({ children }: Props) => {
+  const [version, setVersion] = useState(0);
+  return <PipelineContext.Provider value={{ version, setVersion 
}}>{children}</PipelineContext.Provider>;
+};
 
-export const pipelineRerun = (id: ID) =>
-  request(`/pipelines/${id}/rerun`, {
-    method: 'post',
-  });
-
-export const taskRerun = (id: ID) =>
-  request(`/tasks/${id}/rerun`, {
-    method: 'post',
-  });
+export const usePipeline = () => useContext(PipelineContext);
diff --git a/config-ui/src/pages/pipeline/components/historical/index.tsx 
b/config-ui/src/pages/pipeline/components/historical/index.tsx
index ef6b0d8ca..1c8b8c7cf 100644
--- a/config-ui/src/pages/pipeline/components/historical/index.tsx
+++ b/config-ui/src/pages/pipeline/components/historical/index.tsx
@@ -17,39 +17,41 @@
  */
 
 import React, { useState, useMemo } from 'react';
-import { Icon, ButtonGroup, Button, Position, Intent, IconName } from 
'@blueprintjs/core';
-import { Tooltip2 } from '@blueprintjs/popover2';
+import { ButtonGroup } from '@blueprintjs/core';
 import { pick } from 'lodash';
 import { saveAs } from 'file-saver';
 
 import { DEVLAKE_ENDPOINT } from '@/config';
-import { Card, Loading, Table, ColumnType, Inspector } from '@/components';
+import type { ColumnType } from '@/components';
+import { Card, Loading, Table, Inspector, Dialog, IconButton } from 
'@/components';
 import { useAutoRefresh } from '@/hooks';
 import { formatTime } from '@/utils';
 
 import type { PipelineType } from '../../types';
 import { StatusEnum } from '../../types';
-import { STATUS_ICON, STATUS_LABEL, STATUS_CLS } from '../../misc';
 import * as API from '../../api';
 
+import { usePipeline } from '../context';
+import { PipelineStatus } from '../status';
 import { PipelineDuration } from '../duration';
-
-import * as S from './styled';
+import { PipelineTasks } from '../tasks';
 
 interface Props {
   blueprintId: ID;
 }
 
 export const PipelineHistorical = ({ blueprintId }: Props) => {
-  const [isOpen, setIsOpen] = useState(false);
-  const [json, setJson] = useState<any>({});
+  const [JSON, setJSON] = useState<any>(null);
+  const [ID, setID] = useState<ID | null>(null);
+
+  const { version } = usePipeline();
 
   const { loading, data } = useAutoRefresh<PipelineType[]>(
     async () => {
       const res = await API.getPipelineHistorical(blueprintId);
       return res.pipelines;
     },
-    [],
+    [version],
     {
       cancel: (data) =>
         !!(
@@ -61,6 +63,10 @@ export const PipelineHistorical = ({ blueprintId }: Props) 
=> {
     },
   );
 
+  const handleShowJSON = (row: PipelineType) => {
+    setJSON(pick(row, ['id', 'name', 'plan', 'skipOnFail']));
+  };
+
   const handleDownloadLog = async (id: ID) => {
     const res = await API.getPipelineLog(id);
     if (res) {
@@ -68,6 +74,10 @@ export const PipelineHistorical = ({ blueprintId }: Props) 
=> {
     }
   };
 
+  const handleShowDetails = (id: ID) => {
+    setID(id);
+  };
+
   const columns = useMemo(
     () =>
       [
@@ -75,33 +85,27 @@ export const PipelineHistorical = ({ blueprintId }: Props) 
=> {
           title: 'Status',
           dataIndex: 'status',
           key: 'status',
-          render: (val: StatusEnum) => (
-            <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>
-            </S.StatusColumn>
-          ),
+          render: (val) => <PipelineStatus status={val} />,
         },
         {
           title: 'Started at',
           dataIndex: 'beganAt',
           key: 'beganAt',
+          align: 'center',
           render: (val: string | null) => (val ? formatTime(val) : '-'),
         },
         {
           title: 'Completed at',
           dataIndex: 'finishedAt',
           key: 'finishedAt',
+          align: 'center',
           render: (val: string | null) => (val ? formatTime(val) : '-'),
         },
         {
           title: 'Duration',
           dataIndex: ['status', 'beganAt', 'finishedAt'],
           key: 'duration',
+          align: 'center',
           render: ({ status, beganAt, finishedAt }) => (
             <PipelineDuration status={status} beganAt={beganAt} 
finishedAt={finishedAt} />
           ),
@@ -110,23 +114,12 @@ export const PipelineHistorical = ({ blueprintId }: 
Props) => {
           title: '',
           dataIndex: 'id',
           key: 'action',
+          align: 'center',
           render: (id: ID, row) => (
             <ButtonGroup>
-              <Tooltip2 position={Position.TOP} intent={Intent.PRIMARY} 
content="View JSON">
-                <Button
-                  minimal
-                  intent={Intent.PRIMARY}
-                  icon="code"
-                  onClick={() => {
-                    setIsOpen(true);
-                    setJson(pick(row, ['id', 'name', 'plan', 'skipOnFail']));
-                  }}
-                />
-              </Tooltip2>
-              <Tooltip2 position={Position.TOP} intent={Intent.PRIMARY} 
content="Download Logs">
-                <Button minimal intent={Intent.PRIMARY} icon="document" 
onClick={() => handleDownloadLog(id)} />
-              </Tooltip2>
-              {/* <Button minimal intent={Intent.PRIMARY} icon='chevron-right' 
/> */}
+              <IconButton icon="code" tooltip="View JSON" onClick={() => 
handleShowJSON(row)} />
+              <IconButton icon="document" tooltip="Download Logs" onClick={() 
=> handleDownloadLog(id)} />
+              <IconButton icon="chevron-right" tooltip="View Details" 
onClick={() => handleShowDetails(id)} />
             </ButtonGroup>
           ),
         },
@@ -149,7 +142,12 @@ export const PipelineHistorical = ({ blueprintId }: Props) 
=> {
   return (
     <div>
       <Table columns={columns} dataSource={data} />
-      <Inspector isOpen={isOpen} title={`Pipeline ${json?.id}`} data={json} 
onClose={() => setIsOpen(false)} />
+      {JSON && <Inspector isOpen title={`Pipeline ${JSON?.id}`} data={JSON} 
onClose={() => setJSON(null)} />}
+      {ID && (
+        <Dialog style={{ width: 720 }} isOpen title={`Pipeline ${ID}`} 
footer={null} onCancel={() => setID(null)}>
+          <PipelineTasks id={ID} />
+        </Dialog>
+      )}
     </div>
   );
 };
diff --git a/config-ui/src/pages/pipeline/components/index.ts 
b/config-ui/src/pages/pipeline/components/index.ts
index 294a8fde5..f1374d327 100644
--- a/config-ui/src/pages/pipeline/components/index.ts
+++ b/config-ui/src/pages/pipeline/components/index.ts
@@ -16,5 +16,7 @@
  *
  */
 
+export * from './context';
 export * from './historical';
-export * from './duration';
+export * from './info';
+export * from './tasks';
diff --git a/config-ui/src/pages/pipeline/components/info/index.tsx 
b/config-ui/src/pages/pipeline/components/info/index.tsx
new file mode 100644
index 000000000..146cfa793
--- /dev/null
+++ b/config-ui/src/pages/pipeline/components/info/index.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 from 'react';
+
+import { Loading } from '@/components';
+import { useAutoRefresh } from '@/hooks';
+import { formatTime } from '@/utils';
+
+import type { PipelineType } from '../../types';
+import { StatusEnum } from '../../types';
+import * as API from '../../api';
+
+import { usePipeline } from '../context';
+import { PipelineStatus } from '../status';
+import { PipelineDuration } from '../duration';
+import { PipelineCancel } from '../cancel';
+import { PipelineRerun } from '../rerun';
+
+import * as S from './styled';
+
+interface Props {
+  id: ID;
+  style?: React.CSSProperties;
+}
+
+export const PipelineInfo = ({ id, style }: Props) => {
+  const { version } = usePipeline();
+
+  const { loading, data } = useAutoRefresh<PipelineType>(() => 
API.getPipeline(id), [version], {
+    cancel: (data) => {
+      return !!(
+        data &&
+        [StatusEnum.COMPLETED, StatusEnum.PARTIAL, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(data.status)
+      );
+    },
+  });
+
+  if (loading || !data) {
+    return <Loading />;
+  }
+
+  const { status, beganAt, finishedAt, stage, finishedTasks, totalTasks, 
message } = data;
+
+  return (
+    <S.Wrapper style={style}>
+      <ul>
+        <li>
+          <span>Status</span>
+          <strong>
+            <PipelineStatus status={status} />
+          </strong>
+        </li>
+        <li>
+          <span>Started at</span>
+          <strong>{formatTime(beganAt)}</strong>
+        </li>
+        <li>
+          <span>Duration</span>
+          <strong>
+            <PipelineDuration status={status} beganAt={beganAt} 
finishedAt={finishedAt} />
+          </strong>
+        </li>
+        <li>
+          <span>Current Stage</span>
+          <strong>{stage}</strong>
+        </li>
+        <li>
+          <span>Tasks Completed</span>
+          <strong>
+            {finishedTasks}/{totalTasks}
+          </strong>
+        </li>
+        <li>
+          <PipelineCancel id={id} status={status} />
+          <PipelineRerun type="pipeline" id={id} status={status} />
+        </li>
+      </ul>
+      {StatusEnum.FAILED === status && <p className="'message'">{message}</p>}
+    </S.Wrapper>
+  );
+};
diff --git a/config-ui/src/pages/pipeline/components/historical/styled.ts 
b/config-ui/src/pages/pipeline/components/info/styled.ts
similarity index 63%
copy from config-ui/src/pages/pipeline/components/historical/styled.ts
copy to config-ui/src/pages/pipeline/components/info/styled.ts
index 21362c88b..6760c1f22 100644
--- a/config-ui/src/pages/pipeline/components/historical/styled.ts
+++ b/config-ui/src/pages/pipeline/components/info/styled.ts
@@ -19,28 +19,42 @@
 import { Colors } from '@blueprintjs/core';
 import styled from 'styled-components';
 
-export const StatusColumn = styled.div`
-  display: flex;
-  align-items: center;
+import { Card } from '@/components';
 
-  .bp4-icon {
-    margin-right: 4px;
+export const Wrapper = styled(Card)`
+  ul {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    display: flex;
+    align-items: center;
   }
 
-  &.ready,
-  &.cancel {
-    color: #94959f;
-  }
+  li {
+    flex: 5;
+    display: flex;
+    flex-direction: column;
 
-  &.loading {
-    color: #7497f7;
-  }
+    &:last-child {
+      flex: 1;
+    }
+
+    & > span {
+      font-size: 12px;
+      color: #94959f;
+      text-align: center;
+    }
 
-  &.success {
-    color: ${Colors.GREEN3};
+    & > strong {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-top: 8px;
+    }
   }
 
-  &.error {
+  p.message {
+    margin: 8px 0 0;
     color: ${Colors.RED3};
   }
 `;
diff --git a/config-ui/src/pages/pipeline/components/rerun/index.tsx 
b/config-ui/src/pages/pipeline/components/rerun/index.tsx
new file mode 100644
index 000000000..89f0200f1
--- /dev/null
+++ b/config-ui/src/pages/pipeline/components/rerun/index.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 from 'react';
+
+import { IconButton } from '@/components';
+import { useOperator } from '@/hooks';
+
+import { StatusEnum } from '../../types';
+import * as API from '../../api';
+
+import { usePipeline } from '../context';
+
+interface Props {
+  type: 'pipeline' | 'task';
+  id: ID;
+  status: StatusEnum;
+}
+
+export const PipelineRerun = ({ type, id, status }: Props) => {
+  const { setVersion } = usePipeline();
+
+  const { operating, onSubmit } = useOperator(() => (type === 'task' ? 
API.taskRerun(id) : API.pipelineRerun(id)), {
+    callback: () => setVersion((v) => v + 1),
+  });
+
+  if (![StatusEnum.COMPLETED, StatusEnum.PARTIAL, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(status)) {
+    return null;
+  }
+
+  if (type === 'task') {
+    return <IconButton loading={operating} icon="repeat" tooltip="Rerun task" 
onClick={onSubmit} />;
+  }
+
+  return <IconButton loading={operating} icon="repeat" tooltip="Rerun failed 
tasks" onClick={onSubmit} />;
+};
diff --git a/config-ui/src/pages/pipeline/misc.ts 
b/config-ui/src/pages/pipeline/components/status/index.tsx
similarity index 74%
rename from config-ui/src/pages/pipeline/misc.ts
rename to config-ui/src/pages/pipeline/components/status/index.tsx
index f96499cab..f93355252 100644
--- a/config-ui/src/pages/pipeline/misc.ts
+++ b/config-ui/src/pages/pipeline/components/status/index.tsx
@@ -16,9 +16,15 @@
  *
  */
 
+import React from 'react';
+import { Icon, IconName } from '@blueprintjs/core';
 import classNames from 'classnames';
 
-import { StatusEnum } from './types';
+import { Loading } from '@/components';
+
+import { StatusEnum } from '../../types';
+
+import * as S from './styled';
 
 export const STATUS_ICON = {
   [StatusEnum.CREATED]: 'stopwatch',
@@ -44,11 +50,27 @@ export const STATUS_LABEL = {
   [StatusEnum.CANCELLED]: 'Cancelled',
 };
 
-export const STATUS_CLS = (status: StatusEnum) =>
-  classNames({
+interface Props {
+  status: StatusEnum;
+}
+
+export const PipelineStatus = ({ status }: Props) => {
+  const statusCls = classNames({
     ready: [StatusEnum.CREATED, StatusEnum.PENDING].includes(status),
     loading: [StatusEnum.ACTIVE, StatusEnum.RUNNING, 
StatusEnum.RERUN].includes(status),
     success: [StatusEnum.COMPLETED, StatusEnum.PARTIAL].includes(status),
     error: status === StatusEnum.FAILED,
     cancel: status === StatusEnum.CANCELLED,
   });
+
+  return (
+    <S.Wrapper className={statusCls}>
+      {STATUS_ICON[status] === 'loading' ? (
+        <Loading style={{ marginRight: 4 }} size={14} />
+      ) : (
+        <Icon style={{ marginRight: 4 }} icon={STATUS_ICON[status] as 
IconName} />
+      )}
+      <span>{STATUS_LABEL[status]}</span>
+    </S.Wrapper>
+  );
+};
diff --git a/config-ui/src/pages/pipeline/components/historical/styled.ts 
b/config-ui/src/pages/pipeline/components/status/styled.ts
similarity index 89%
rename from config-ui/src/pages/pipeline/components/historical/styled.ts
rename to config-ui/src/pages/pipeline/components/status/styled.ts
index 21362c88b..87ed2db8c 100644
--- a/config-ui/src/pages/pipeline/components/historical/styled.ts
+++ b/config-ui/src/pages/pipeline/components/status/styled.ts
@@ -19,14 +19,7 @@
 import { Colors } from '@blueprintjs/core';
 import styled from 'styled-components';
 
-export const StatusColumn = styled.div`
-  display: flex;
-  align-items: center;
-
-  .bp4-icon {
-    margin-right: 4px;
-  }
-
+export const Wrapper = styled.div`
   &.ready,
   &.cancel {
     color: #94959f;
diff --git a/config-ui/src/pages/pipeline/detail/components/task/index.tsx 
b/config-ui/src/pages/pipeline/components/task/index.tsx
similarity index 64%
rename from config-ui/src/pages/pipeline/detail/components/task/index.tsx
rename to config-ui/src/pages/pipeline/components/task/index.tsx
index f8b868dd7..8ec91ee9a 100644
--- a/config-ui/src/pages/pipeline/detail/components/task/index.tsx
+++ b/config-ui/src/pages/pipeline/components/task/index.tsx
@@ -17,26 +17,26 @@
  */
 
 import React, { useMemo } from 'react';
-import { Icon, Intent, Position, Colors } from '@blueprintjs/core';
-import { Tooltip2 } from '@blueprintjs/popover2';
+import { Intent } from '@blueprintjs/core';
 
+import { TextTooltip } from '@/components';
 import type { PluginConfigType } from '@/plugins';
 import { Plugins, PluginConfig } from '@/plugins';
 
-import { PipelineDuration } from '../../../components';
-import { StatusEnum, TaskType } from '../../../types';
-import { STATUS_CLS } from '../../../misc';
+import type { TaskType } from '../../types';
+import { StatusEnum } from '../../types';
+
+import { PipelineDuration } from '../duration';
+import { PipelineRerun } from '../rerun';
 
 import * as S from './styled';
 
 interface Props {
   task: TaskType;
-  operating: boolean;
-  onRerun: (id: ID) => void;
 }
 
-export const Task = ({ task, operating, onRerun }: Props) => {
-  const { beganAt, finishedAt, status, message, progressDetail } = task;
+export const PipelineTask = ({ task }: Props) => {
+  const { id, beganAt, finishedAt, status, message, progressDetail } = task;
 
   const [icon, name] = useMemo(() => {
     const config = PluginConfig.find((p) => p.plugin === task.plugin) as 
PluginConfigType;
@@ -57,32 +57,28 @@ export const Task = ({ task, operating, onRerun }: Props) 
=> {
       case Plugins.GitLab === config.plugin:
         name = `${name}:projectId:${options.projectId}`;
         break;
+      case [Plugins.JIRA, Plugins.Jenkins].includes(config.plugin):
+        name = `${name}:${options.scopeId}`;
+        break;
     }
 
     return [config.icon, name];
   }, [task]);
 
-  const statusCls = STATUS_CLS(status);
-
-  const handleRerun = () => {
-    if (operating) return;
-    onRerun(task.id);
-  };
-
   return (
     <S.Wrapper>
       <S.Info>
         <div className="title">
           <img src={icon} alt="" />
           <strong>Task{task.id}</strong>
-          <span title={name}>{name}</span>
+          <span>
+            <TextTooltip content={name}>{name}</TextTooltip>
+          </span>
         </div>
-        {[status === StatusEnum.CREATED, StatusEnum.PENDING].includes(status) 
&& (
-          <p className={statusCls}>Subtasks pending</p>
-        )}
+        {[status === StatusEnum.CREATED, StatusEnum.PENDING].includes(status) 
&& <p>Subtasks pending</p>}
 
         {[StatusEnum.ACTIVE, StatusEnum.RUNNING].includes(status) && (
-          <p className={statusCls}>
+          <p>
             Subtasks running
             <strong style={{ marginLeft: 8 }}>
               
{progressDetail?.finishedSubTasks}/{progressDetail?.totalSubTasks}
@@ -90,23 +86,19 @@ export const Task = ({ task, operating, onRerun }: Props) 
=> {
           </p>
         )}
 
-        {status === StatusEnum.COMPLETED && <p className={statusCls}>All 
Subtasks completed</p>}
+        {status === StatusEnum.COMPLETED && <p>All Subtasks completed</p>}
 
         {status === StatusEnum.FAILED && (
-          <Tooltip2 content={message} intent={Intent.DANGER}>
-            <p className={statusCls}>Task failed: hover to view the reason</p>
-          </Tooltip2>
+          <TextTooltip intent={Intent.DANGER} content={message}>
+            <p className="error">Task failed: hover to view the reason</p>
+          </TextTooltip>
         )}
 
-        {status === StatusEnum.CANCELLED && <p className={statusCls}>Subtasks 
canceled</p>}
+        {status === StatusEnum.CANCELLED && <p>Subtasks canceled</p>}
       </S.Info>
       <S.Duration>
-        {[StatusEnum.COMPLETED, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(status) && (
-          <Tooltip2 position={Position.TOP} content="Rerun task">
-            <Icon icon="repeat" style={{ color: Colors.BLUE3 }} 
onClick={handleRerun} />
-          </Tooltip2>
-        )}
         <PipelineDuration status={status} beganAt={beganAt} 
finishedAt={finishedAt} />
+        <PipelineRerun type="task" id={id} status={status} />
       </S.Duration>
     </S.Wrapper>
   );
diff --git a/config-ui/src/pages/pipeline/detail/components/task/styled.ts 
b/config-ui/src/pages/pipeline/components/task/styled.ts
similarity index 94%
rename from config-ui/src/pages/pipeline/detail/components/task/styled.ts
rename to config-ui/src/pages/pipeline/components/task/styled.ts
index f15f0fa40..35b153954 100644
--- a/config-ui/src/pages/pipeline/detail/components/task/styled.ts
+++ b/config-ui/src/pages/pipeline/components/task/styled.ts
@@ -38,18 +38,17 @@ export const Info = styled.div`
     align-items: center;
     margin-bottom: 8px;
 
-    img {
+    & > img {
       width: 20px;
     }
 
-    strong {
+    & > strong {
       margin: 0 4px;
     }
 
-    span {
+    & > span {
+      flex: auto;
       overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
     }
   }
 
diff --git a/config-ui/src/pages/pipeline/components/tasks/index.tsx 
b/config-ui/src/pages/pipeline/components/tasks/index.tsx
new file mode 100644
index 000000000..82eb0d4d2
--- /dev/null
+++ b/config-ui/src/pages/pipeline/components/tasks/index.tsx
@@ -0,0 +1,125 @@
+/*
+ * 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 { Icon, Collapse, Button } from '@blueprintjs/core';
+import { sortBy, groupBy } from 'lodash';
+
+import { Loading } from '@/components';
+import { useAutoRefresh } from '@/hooks';
+
+import type { TaskType } from '../../types';
+import { StatusEnum } from '../../types';
+import * as API from '../../api';
+
+import { usePipeline } from '../context';
+import { PipelineTask } from '../task';
+
+import * as S from './styled';
+
+interface Props {
+  id: ID;
+  style?: React.CSSProperties;
+}
+
+export const PipelineTasks = ({ id, style }: Props) => {
+  const [isOpen, setIsOpen] = useState(true);
+
+  const { version } = usePipeline();
+
+  const { loading, data } = useAutoRefresh<TaskType[]>(
+    async () => {
+      const taskRes = await API.getPipelineTasks(id);
+      return taskRes.tasks;
+    },
+    [version],
+    {
+      cancel: (data) => {
+        return !!(
+          data &&
+          data.every((task) => [StatusEnum.COMPLETED, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(task.status))
+        );
+      },
+    },
+  );
+
+  if (loading) {
+    return <Loading />;
+  }
+
+  const stages = groupBy(sortBy(data, 'id'), 'pipelineRow');
+
+  const handleToggleOpen = () => setIsOpen(!isOpen);
+
+  return (
+    <S.Wrapper style={style}>
+      <S.Inner>
+        <S.Header>
+          {Object.keys(stages).map((key) => {
+            let status;
+
+            switch (true) {
+              case !!stages[key].find((task) => [StatusEnum.ACTIVE, 
StatusEnum.RUNNING].includes(task.status)):
+                status = 'loading';
+                break;
+              case stages[key].every((task) => task.status === 
StatusEnum.COMPLETED):
+                status = 'success';
+                break;
+              case !!stages[key].find((task) => task.status === 
StatusEnum.FAILED):
+                status = 'error';
+                break;
+              case !!stages[key].find((task) => task.status === 
StatusEnum.CANCELLED):
+                status = 'cancel';
+                break;
+              default:
+                status = 'ready';
+                break;
+            }
+
+            return (
+              <li key={key} className={status}>
+                <strong>Stage {key}</strong>
+                {status === 'loading' && <Loading size={14} />}
+                {status === 'success' && <Icon icon="tick-circle" />}
+                {status === 'error' && <Icon icon="cross-circle" />}
+                {status === 'cancel' && <Icon icon="disable" />}
+              </li>
+            );
+          })}
+        </S.Header>
+        <Collapse isOpen={isOpen}>
+          <S.Tasks>
+            {Object.keys(stages).map((key) => (
+              <li key={key}>
+                {stages[key].map((task) => (
+                  <PipelineTask key={task.id} task={task} />
+                ))}
+              </li>
+            ))}
+          </S.Tasks>
+        </Collapse>
+      </S.Inner>
+      <Button
+        className="collapse-control"
+        minimal
+        icon={isOpen ? 'chevron-down' : 'chevron-up'}
+        onClick={handleToggleOpen}
+      />
+    </S.Wrapper>
+  );
+};
diff --git a/config-ui/src/pages/pipeline/detail/styled.ts 
b/config-ui/src/pages/pipeline/components/tasks/styled.ts
similarity index 66%
rename from config-ui/src/pages/pipeline/detail/styled.ts
rename to config-ui/src/pages/pipeline/components/tasks/styled.ts
index 1aa2ef89f..1d1f7b3da 100644
--- a/config-ui/src/pages/pipeline/detail/styled.ts
+++ b/config-ui/src/pages/pipeline/components/tasks/styled.ts
@@ -16,72 +16,16 @@
  *
  */
 
-import { Colors } from '@blueprintjs/core';
 import styled from 'styled-components';
 
 export const Wrapper = styled.div`
-  .card + .card {
-    margin-top: 16px;
-  }
-
-  .card:last-child {
-    position: relative;
-    padding: 24px 48px 24px 24px;
-
-    .collapse-control {
-      position: absolute;
-      right: 12px;
-      top: 24px;
-    }
-  }
-
-  p.message {
-    margin: 8px 0 0;
-
-    &.error {
-      color: ${Colors.RED3};
-    }
-  }
-`;
-
-export const Pipeline = styled.ul`
-  margin: 0;
-  padding: 0;
-  list-style: none;
-  display: flex;
-  align-items: center;
-
-  li {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-
-    &.success {
-      color: ${Colors.GREEN3};
-
-      .bp4-icon {
-        color: ${Colors.GREEN3};
-      }
-    }
+  position: relative;
+  padding-right: 36px;
 
-    &.error {
-      color: ${Colors.RED3};
-
-      .bp4-icon {
-        color: ${Colors.RED3};
-      }
-    }
-
-    & > span {
-      font-size: 12px;
-      color: #94959f;
-    }
-
-    & > strong {
-      display: flex;
-      align-items: center;
-      margin-top: 8px;
-    }
+  .collapse-control {
+    position: absolute;
+    right: 0;
+    top: 0;
   }
 `;
 
diff --git a/config-ui/src/pages/pipeline/detail/components/index.ts 
b/config-ui/src/pages/pipeline/detail/components/index.ts
deleted file mode 100644
index 7636f4330..000000000
--- a/config-ui/src/pages/pipeline/detail/components/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-export * from './task';
diff --git a/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx 
b/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx
deleted file mode 100644
index 25ff9377a..000000000
--- a/config-ui/src/pages/pipeline/detail/pipeline-detail.tsx
+++ /dev/null
@@ -1,195 +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 } 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 } from '@/utils';
-
-import { PipelineDuration } from '../components';
-import type { PipelineType, TaskType } from '../types';
-import { StatusEnum } from '../types';
-import { STATUS_ICON, STATUS_LABEL, STATUS_CLS } from '../misc';
-
-import { useDetail } from './use-detail';
-import { Task } from './components';
-import * as API from './api';
-import * as S from './styled';
-
-interface Props {
-  id: ID;
-}
-
-export const PipelineDetail = ({ id }: Props) => {
-  const [isOpen, setIsOpen] = useState(true);
-
-  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.PARTIAL, StatusEnum.FAILED, 
StatusEnum.CANCELLED].includes(pipeline.status)
-        );
-      },
-    },
-  );
-
-  if (loading || !data) {
-    return <Loading />;
-  }
-
-  const { pipeline, tasks } = data;
-
-  const { status, beganAt, finishedAt, stage, finishedTasks, totalTasks, 
message } = pipeline;
-  const stages = groupBy(tasks, 'pipelineRow');
-
-  const handleToggleOpen = () => {
-    setIsOpen(!isOpen);
-  };
-
-  const statusCls = STATUS_CLS(status);
-
-  return (
-    <S.Wrapper>
-      <Card className="card">
-        <S.Pipeline>
-          <li className={statusCls}>
-            <span>Status</span>
-            <strong>
-              {STATUS_ICON[status] === 'loading' ? (
-                <Loading style={{ marginRight: 4 }} size={14} />
-              ) : (
-                <Icon style={{ marginRight: 4 }} icon={STATUS_ICON[status] as 
IconName} />
-              )}
-              {STATUS_LABEL[status]}
-            </strong>
-          </li>
-          <li>
-            <span>Started at</span>
-            <strong>{formatTime(beganAt)}</strong>
-          </li>
-          <li>
-            <span>Duration</span>
-            <strong>
-              <PipelineDuration status={status} beganAt={beganAt} 
finishedAt={finishedAt} />
-            </strong>
-          </li>
-          <li>
-            <span>Current Stage</span>
-            <strong>{stage}</strong>
-          </li>
-          <li>
-            <span>Tasks Completed</span>
-            <strong>
-              {finishedTasks}/{totalTasks}
-            </strong>
-          </li>
-          <li>
-            {[StatusEnum.CREATED, StatusEnum.PENDING, StatusEnum.ACTIVE, 
StatusEnum.RUNNING].includes(status) && (
-              <Button loading={operating} outlined intent={Intent.PRIMARY} 
text="Cancel" onClick={onCancel} />
-            )}
-
-            {[StatusEnum.FAILED, StatusEnum.PARTIAL].includes(status) && (
-              <Button
-                loading={operating}
-                outlined
-                intent={Intent.PRIMARY}
-                text="Rerun failed tasks"
-                onClick={onRerun}
-              />
-            )}
-          </li>
-        </S.Pipeline>
-        {StatusEnum.FAILED === status && <p className={classNames('message', 
statusCls)}>{message}</p>}
-      </Card>
-      <Card className="card">
-        <S.Inner>
-          <S.Header>
-            {Object.keys(stages).map((key) => {
-              let status;
-
-              switch (true) {
-                case !!stages[key].find((task) => [StatusEnum.ACTIVE, 
StatusEnum.RUNNING].includes(task.status)):
-                  status = 'loading';
-                  break;
-                case stages[key].every((task) => [StatusEnum.COMPLETED, 
StatusEnum.PARTIAL].includes(task.status)):
-                  status = 'success';
-                  break;
-                case !!stages[key].find((task) => task.status === 
StatusEnum.FAILED):
-                  status = 'error';
-                  break;
-                case !!stages[key].find((task) => task.status === 
StatusEnum.CANCELLED):
-                  status = 'cancel';
-                  break;
-                default:
-                  status = 'ready';
-                  break;
-              }
-
-              return (
-                <li key={key} className={status}>
-                  <strong>Stage {key}</strong>
-                  {status === 'loading' && <Loading size={14} />}
-                  {status === 'success' && <Icon icon="tick-circle" />}
-                  {status === 'error' && <Icon icon="cross-circle" />}
-                  {status === 'cancel' && <Icon icon="disable" />}
-                </li>
-              );
-            })}
-          </S.Header>
-          <Collapse isOpen={isOpen}>
-            <S.Tasks>
-              {Object.keys(stages).map((key) => (
-                <li key={key}>
-                  {stages[key].map((task) => (
-                    <Task key={task.id} task={task} operating={operating} 
onRerun={onRerunTask} />
-                  ))}
-                </li>
-              ))}
-            </S.Tasks>
-          </Collapse>
-        </S.Inner>
-        <Button
-          className="collapse-control"
-          minimal
-          icon={isOpen ? 'chevron-down' : 'chevron-up'}
-          onClick={handleToggleOpen}
-        />
-      </Card>
-    </S.Wrapper>
-  );
-};
diff --git a/config-ui/src/pages/pipeline/index.ts 
b/config-ui/src/pages/pipeline/index.ts
index 5c45114f7..eecb8b531 100644
--- a/config-ui/src/pages/pipeline/index.ts
+++ b/config-ui/src/pages/pipeline/index.ts
@@ -17,6 +17,4 @@
  */
 
 export * from './types';
-export * from './misc';
 export * from './components';
-export * from './detail';

Reply via email to