You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by ab...@apache.org on 2024/03/29 09:44:36 UTC
(incubator-devlake) 13/40: feat: add onboard finish status (#7218)
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: 青湛 <0x...@gmail.com>
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>
</>
);
};