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

abeizn pushed a commit to branch release-v1.0
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git

commit 0a7410c583a3455e456b7eb10bdb2932ff108c93
Author: 青湛 <[email protected]>
AuthorDate: Tue Mar 26 21:08:49 2024 +1300

    feat: add onboard finish status (#7218)
---
 config-ui/src/api/pipeline/index.ts                |   2 +
 .../components/connection-name/index.tsx}          |  45 +++--
 config-ui/src/plugins/components/index.ts          |   1 +
 .../onboard}/components/index.ts                   |   9 +-
 config-ui/src/routes/onboard/components/logs.tsx   | 105 ++++++++++
 config-ui/src/routes/onboard/context.tsx           |  14 +-
 config-ui/src/routes/onboard/index.tsx             |   3 +-
 config-ui/src/routes/onboard/step-2.tsx            |   2 +-
 config-ui/src/routes/onboard/step-3.tsx            |  11 +-
 config-ui/src/routes/onboard/step-4.tsx            | 211 ++++++++++++++++++++-
 10 files changed, 360 insertions(+), 43 deletions(-)

diff --git a/config-ui/src/api/pipeline/index.ts 
b/config-ui/src/api/pipeline/index.ts
index 1ec8e025c..2d7261d57 100644
--- a/config-ui/src/api/pipeline/index.ts
+++ b/config-ui/src/api/pipeline/index.ts
@@ -36,3 +36,5 @@ export const rerun = (id: ID) =>
 export const log = (id: ID) => request(`/pipelines/${id}/logging.tar.gz`);
 
 export const tasks = (id: ID) => request(`/pipelines/${id}/tasks`);
+
+export const subTasks = (id: ID) => request(`/pipelines/${id}/subtasks`);
diff --git a/config-ui/src/routes/onboard/step-4.tsx 
b/config-ui/src/plugins/components/connection-name/index.tsx
similarity index 61%
copy from config-ui/src/routes/onboard/step-4.tsx
copy to config-ui/src/plugins/components/connection-name/index.tsx
index e2d51cfed..fd3a59d3a 100644
--- a/config-ui/src/routes/onboard/step-4.tsx
+++ b/config-ui/src/plugins/components/connection-name/index.tsx
@@ -16,38 +16,43 @@
  *
  */
 
-import { SmileFilled } from '@ant-design/icons';
+import { useMemo } from 'react';
 import { theme } from 'antd';
 import styled from 'styled-components';
 
-import * as S from './styled';
+import { getPluginConfig } from '@/plugins';
 
-const Top = styled.div`
+const Wrapper = styled.div`
   display: flex;
   align-items: center;
-  justify-content: center;
-  margin-top: 100px;
-  margin-bottom: 24px;
-  height: 70px;
-
-  span.text {
-    margin-left: 8px;
-    font-size: 20px;
+
+  .icon {
+    display: inline-flex;
+    margin-right: 8px;
+    width: 24px;
+
+    & > svg {
+      width: 100%;
+      height: 100%;
+    }
   }
 `;
 
-export const Step4 = () => {
+interface Props {
+  plugin: string;
+}
+
+export const ConnectionName = ({ plugin }: Props) => {
+  const config = useMemo(() => getPluginConfig(plugin), [plugin]);
+
   const {
-    token: { green5 },
+    token: { colorPrimary },
   } = theme.useToken();
 
   return (
-    <>
-      <Top>
-        <SmileFilled style={{ fontSize: 36, color: green5 }} />
-        <span className="text">Congratulations!You have successfully connected 
to your first repository!</span>
-      </Top>
-      <S.StepContent style={{ padding: 24 }}></S.StepContent>
-    </>
+    <Wrapper>
+      <span className="icon">{config.icon({ color: colorPrimary })}</span>
+      <span className="name">Manage Connections: {config.name}</span>
+    </Wrapper>
   );
 };
diff --git a/config-ui/src/plugins/components/index.ts 
b/config-ui/src/plugins/components/index.ts
index 2f4049cab..7b4e89a9e 100644
--- a/config-ui/src/plugins/components/index.ts
+++ b/config-ui/src/plugins/components/index.ts
@@ -18,6 +18,7 @@
 
 export * from './connection-form';
 export * from './connection-list';
+export * from './connection-name';
 export * from './connection-select';
 export * from './connection-status';
 export * from './data-scope-remote';
diff --git a/config-ui/src/plugins/components/index.ts 
b/config-ui/src/routes/onboard/components/index.ts
similarity index 73%
copy from config-ui/src/plugins/components/index.ts
copy to config-ui/src/routes/onboard/components/index.ts
index 2f4049cab..49ecf41b7 100644
--- a/config-ui/src/plugins/components/index.ts
+++ b/config-ui/src/routes/onboard/components/index.ts
@@ -16,11 +16,4 @@
  *
  */
 
-export * from './connection-form';
-export * from './connection-list';
-export * from './connection-select';
-export * from './connection-status';
-export * from './data-scope-remote';
-export * from './data-scope-select';
-export * from './scope-config-form';
-export * from './scope-config-select';
+export * from './logs';
diff --git a/config-ui/src/routes/onboard/components/logs.tsx 
b/config-ui/src/routes/onboard/components/logs.tsx
new file mode 100644
index 000000000..438254b70
--- /dev/null
+++ b/config-ui/src/routes/onboard/components/logs.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 { LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined } from 
'@ant-design/icons';
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+  padding: 10px 20px;
+  font-size: 12px;
+  color: #70727f;
+  background: #f6f6f8;
+
+  .title {
+    font-weight: 600;
+  }
+
+  ul {
+    margin-top: 12px;
+  }
+
+  li {
+    display: flex;
+    margin-top: 6px;
+    position: relative;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  span.name {
+    flex: 1;
+  }
+
+  span.status {
+    flex: 1;
+    text-align: right;
+  }
+
+  span.anticon {
+    position: absolute;
+    right: -15px;
+  }
+`;
+
+interface LogsProps {
+  style?: React.CSSProperties;
+  log: {
+    plugin: string;
+    scopeName: string;
+    status: string;
+    tasks: Array<{
+      step: number;
+      name: string;
+      status: 'pending' | 'running' | 'success' | 'failed';
+      finishedRecords: number;
+    }>;
+  };
+}
+
+export const Logs = ({ style, log: { plugin, scopeName, status, tasks } }: 
LogsProps) => {
+  if (!plugin) {
+    return null;
+  }
+
+  return (
+    <Wrapper style={style}>
+      <div className="title">
+        {plugin}:{scopeName}
+      </div>
+      <ul>
+        {tasks.map((task) => (
+          <li>
+            <span className="name">
+              Step {task.step} - {task.name}
+            </span>
+            {task.status === 'pending' ? (
+              <span className="status">Pending</span>
+            ) : (
+              <span className="status">Records collected: 
{task.finishedRecords}</span>
+            )}
+            {task.status === 'running' && <LoadingOutlined />}
+            {task.status === 'success' && <CheckCircleOutlined />}
+            {task.status === 'failed' && <CloseCircleOutlined />}
+          </li>
+        ))}
+      </ul>
+    </Wrapper>
+  );
+};
diff --git a/config-ui/src/routes/onboard/context.tsx 
b/config-ui/src/routes/onboard/context.tsx
index 7f472d94f..c7aa00c34 100644
--- a/config-ui/src/routes/onboard/context.tsx
+++ b/config-ui/src/routes/onboard/context.tsx
@@ -18,17 +18,21 @@
 
 import { createContext } from 'react';
 
+export type Record = {
+  plugin: string;
+  connectionId: ID;
+  pipelineId: ID;
+  scopeName: string;
+};
+
 const initialValue: {
   step: number;
-  records: Array<{
-    plugin: string;
-    connectionId: ID;
-  }>;
+  records: Record[];
   done: boolean;
   projectName?: string;
   plugin?: string;
   setStep: (value: number) => void;
-  setRecords: (value: Array<{ plugin: string; connectionId: ID }>) => void;
+  setRecords: (value: Record[]) => void;
   setProjectName: (value: string) => void;
   setPlugin: (value: string) => void;
 } = {
diff --git a/config-ui/src/routes/onboard/index.tsx 
b/config-ui/src/routes/onboard/index.tsx
index d2b4d1e6e..addbfc451 100644
--- a/config-ui/src/routes/onboard/index.tsx
+++ b/config-ui/src/routes/onboard/index.tsx
@@ -24,6 +24,7 @@ import API from '@/api';
 import { PageLoading } from '@/components';
 import { useRefreshData } from '@/hooks';
 
+import type { Record } from './context';
 import { Context } from './context';
 import { Step0 } from './step-0';
 import { Step1 } from './step-1';
@@ -49,7 +50,7 @@ const steps = [
 
 export const Onboard = () => {
   const [step, setStep] = useState(0);
-  const [records, setRecords] = useState<Array<{ plugin: string; connectionId: 
ID }>>([]);
+  const [records, setRecords] = useState<Record[]>([]);
   const [projectName, setProjectName] = useState<string>();
   const [plugin, setPlugin] = useState<string>();
 
diff --git a/config-ui/src/routes/onboard/step-2.tsx 
b/config-ui/src/routes/onboard/step-2.tsx
index be4552463..3de264787 100644
--- a/config-ui/src/routes/onboard/step-2.tsx
+++ b/config-ui/src/routes/onboard/step-2.tsx
@@ -96,7 +96,7 @@ export const Step2 = () => {
           ...payload,
         });
 
-        const newRecords = [...records, { plugin, connectionId: connection.id 
}];
+        const newRecords = [...records, { plugin, connectionId: connection.id, 
pipelineId: '', scopeName: '' }];
 
         setRecords(newRecords);
 
diff --git a/config-ui/src/routes/onboard/step-3.tsx 
b/config-ui/src/routes/onboard/step-3.tsx
index c66e3f9ab..3fba3cfff 100644
--- a/config-ui/src/routes/onboard/step-3.tsx
+++ b/config-ui/src/routes/onboard/step-3.tsx
@@ -36,7 +36,7 @@ export const Step3 = () => {
   const [operating, setOperating] = useState(false);
   const [scopes, setScopes] = useState<any[]>([]);
 
-  const { step, records, done, projectName, plugin, setStep } = 
useContext(Context);
+  const { step, records, done, projectName, plugin, setStep, setRecords } = 
useContext(Context);
 
   useEffect(() => {
     fetch(`/onboard/step-3/${plugin}.md`)
@@ -97,17 +97,22 @@ export const Step3 = () => {
         // 4. trigger this blueprint
         await API.blueprint.trigger(blueprint.id, { skipCollectors: false, 
fullSync: false });
 
+        // 5. get current run pipeline
+        const pipeline = await API.blueprint.pipelines(blueprint.id);
+
         const newRecords = records.map((it) =>
           it.plugin !== plugin
             ? it
             : {
                 ...it,
-                scopeId: getPluginScopeId(plugin, scopes[0].data),
+                pipelineId: pipeline.pipelines[0].id,
                 scopeName: scopes[0]?.fullName ?? scopes[0].name,
               },
         );
 
-        // 5. update store
+        setRecords(newRecords);
+
+        // 6. update store
         await API.store.set('onboard', {
           step: 4,
           records: newRecords,
diff --git a/config-ui/src/routes/onboard/step-4.tsx 
b/config-ui/src/routes/onboard/step-4.tsx
index e2d51cfed..eb1b92d6c 100644
--- a/config-ui/src/routes/onboard/step-4.tsx
+++ b/config-ui/src/routes/onboard/step-4.tsx
@@ -16,11 +16,17 @@
  *
  */
 
-import { SmileFilled } from '@ant-design/icons';
-import { theme } from 'antd';
+import { useState, useContext, useMemo } from 'react';
+import { SmileFilled, CheckCircleOutlined, CloseCircleOutlined } from 
'@ant-design/icons';
+import { theme, Progress, Space, Button, Modal } from 'antd';
 import styled from 'styled-components';
 
-import * as S from './styled';
+import API from '@/api';
+import { useAutoRefresh } from '@/hooks';
+import { ConnectionName, ConnectionForm } from '@/plugins';
+
+import { Logs } from './components';
+import { Context } from './context';
 
 const Top = styled.div`
   display: flex;
@@ -36,18 +42,213 @@ const Top = styled.div`
   }
 `;
 
+const Content = styled.div`
+  padding: 24px;
+  background-color: #fff;
+  box-shadow: 0px 2.4px 4.8px -0.8px rgba(0, 0, 0, 0.1), 0px 1.6px 8px 0px 
rgba(0, 0, 0, 0.07);
+
+  .top {
+    margin-bottom: 42px;
+    text-align: center;
+
+    .info {
+      margin-bottom: 34px;
+      font-size: 16px;
+      font-weight: 600;
+    }
+
+    .tip {
+      margin-bottom: 42px;
+      font-size: 12px;
+      color: #818388;
+    }
+
+    .action {
+      margin-top: 30px;
+    }
+  }
+`;
+
+const LogsWrapper = styled.div`
+  .tip {
+    font-size: 12px;
+    font-weight: 600;
+    color: #70727f;
+  }
+
+  .detail {
+    display: flex;
+    margin-top: 12px;
+
+    & > div {
+      flex: 1;
+    }
+  }
+`;
+
+const getStatus = (data: any) => {
+  if (!data) {
+    return 'running';
+  }
+
+  switch (data.status) {
+    case 'TASK_COMPLETED':
+      return 'success';
+    case 'TASK_PARTIAL':
+      return 'partial';
+    case 'TASK_FAILED':
+      return 'failed';
+    case 'TASK_RUNNING':
+    default:
+      return 'running';
+  }
+};
+
 export const Step4 = () => {
+  const [open, setOpen] = useState(false);
+
+  const { records, plugin } = useContext(Context);
+
+  const record = useMemo(() => records.find((it) => it.plugin === plugin), 
[plugin, records]);
+
+  const { data } = useAutoRefresh(
+    async () => {
+      const taskRes = await API.pipeline.subTasks(record?.pipelineId as 
string);
+      return taskRes.tasks;
+    },
+    [],
+    {
+      cancel: (data) => {
+        return !!(data && ['TASK_COMPLETED', 'TASK_PARTIAL', 
'TASK_FAILED'].includes(data.status));
+      },
+    },
+  );
+
+  const [status, percent, collector, extractor] = useMemo(() => {
+    const status = getStatus(data);
+    const percent = (data?.completionRate ?? 0) * 100;
+
+    const collectorTask = (data?.subtasks ?? [])[0] ?? {};
+    const extractorTask = (data?.subtasks ?? [])[1] ?? {};
+
+    const collector = {
+      plugin: collectorTask.plugin,
+      scopeName: collectorTask.option?.name,
+      status: collectorTask.status,
+      tasks: (collectorTask.subtaskDetails ?? [])
+        .filter((it: any) => it.is_collector === '1')
+        .map((it: any) => ({
+          step: it.sequence,
+          name: it.name,
+          status: it.is_failed === '1' ? 'failed' : !it.began_at ? 'pending' : 
it.finished_at ? 'success' : 'running',
+          finishedRecords: it.finished_records,
+        })),
+    };
+
+    const extractor = {
+      plugin: extractorTask.plugin,
+      scopeName: extractorTask.option?.name,
+      status: extractorTask.status,
+      tasks: (extractorTask.subtaskDetails ?? [])
+        .filter((it: any) => it.is_collector === '1')
+        .map((it: any) => ({
+          step: it.sequence,
+          name: it.name,
+          status: it.is_failed === '1' ? 'failed' : !it.began_at ? 'pending' : 
it.finished_at ? 'success' : 'running',
+          finishedRecords: it.finished_records,
+        })),
+    };
+
+    return [status, percent, collector, extractor];
+  }, [data]);
+
   const {
-    token: { green5 },
+    token: { green5, orange5, red5 },
   } = theme.useToken();
 
+  if (!plugin || !record) {
+    return null;
+  }
+
+  const { connectionId, scopeName } = record;
+
   return (
     <>
       <Top>
         <SmileFilled style={{ fontSize: 36, color: green5 }} />
         <span className="text">Congratulations!You have successfully connected 
to your first repository!</span>
       </Top>
-      <S.StepContent style={{ padding: 24 }}></S.StepContent>
+      <Content>
+        {status === 'running' && (
+          <div className="top">
+            <div className="info">syncing up data from {scopeName}...</div>
+            <div className="tip">
+              This may take a few minutes to hours, depending on the size of 
your data and rate limits of the tool you
+              choose. Exit
+            </div>
+            <Progress type="circle" size={120} percent={percent} />
+          </div>
+        )}
+        {status === 'success' && (
+          <div className="top">
+            <div className="info">{scopeName} is successfully collected !</div>
+            <CheckCircleOutlined style={{ fontSize: 120, color: green5 }} />
+            <div className="action">
+              <Space direction="vertical">
+                <Button type="primary">Check Dashboard</Button>
+                <Button type="link">finish</Button>
+              </Space>
+            </div>
+          </div>
+        )}
+        {status === 'partial' && (
+          <div className="top">
+            <div className="info">{scopeName} is parted collected!</div>
+            <CheckCircleOutlined style={{ fontSize: 120, color: orange5 }} />
+            <div className="action">
+              <Space>
+                <Button type="primary">Re-collect Data</Button>
+                <Button type="primary">Check Dashboard</Button>
+              </Space>
+            </div>
+          </div>
+        )}
+        {status === 'failed' && (
+          <div className="top">
+            <div className="info">Something went wrong with the collection 
process. </div>
+            <div className="info">
+              Please check out the
+              <Button type="link" onClick={() => setOpen(true)}>
+                network and token permission
+              </Button>
+              and retry data collection
+            </div>
+            <CloseCircleOutlined style={{ fontSize: 120, color: red5 }} />
+            <div className="action">
+              <Space direction="vertical">
+                <Button type="primary">Re-collect Data</Button>
+              </Space>
+            </div>
+          </div>
+        )}
+        <LogsWrapper>
+          <div className="tip">Sync progress details</div>
+          <div className="detail">
+            <Logs log={collector} />
+            <Logs log={extractor} style={{ marginLeft: 16 }} />
+          </div>
+        </LogsWrapper>
+      </Content>
+      <Modal
+        open={open}
+        width={820}
+        centered
+        title={<ConnectionName plugin={plugin} />}
+        footer={null}
+        onCancel={() => setOpen(false)}
+      >
+        <ConnectionForm plugin={plugin} connectionId={connectionId} 
onSuccess={() => setOpen(false)} />
+      </Modal>
     </>
   );
 };

Reply via email to