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

klesh 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 55135f9d6 refactor: use redux to rewrite onboard (#8012)
55135f9d6 is described below

commit 55135f9d63a708d980edf5dd8b4ddd46c8be20d7
Author: 青湛 <[email protected]>
AuthorDate: Mon Sep 9 14:54:55 2024 +1200

    refactor: use redux to rewrite onboard (#8012)
---
 config-ui/src/app/store.ts                         |   2 +
 .../context.tsx => features/onboard/index.ts}      |  32 +----
 config-ui/src/features/onboard/slice.ts            | 131 +++++++++++++++++++++
 config-ui/src/routes/app/app.tsx                   |  10 +-
 config-ui/src/routes/app/loader.ts                 |   7 --
 config-ui/src/routes/layout/layout.tsx             |  10 +-
 config-ui/src/routes/onboard/components/card.tsx   |  44 +++----
 config-ui/src/routes/onboard/components/tour.tsx   |   8 +-
 config-ui/src/routes/onboard/index.tsx             | 108 ++++++-----------
 config-ui/src/routes/onboard/step-0.tsx            |  40 ++-----
 config-ui/src/routes/onboard/step-1.tsx            |  27 ++---
 config-ui/src/routes/onboard/step-2.tsx            |  54 +++------
 config-ui/src/routes/onboard/step-3.tsx            |  27 ++---
 config-ui/src/routes/onboard/step-4.tsx            |  52 ++------
 14 files changed, 264 insertions(+), 288 deletions(-)

diff --git a/config-ui/src/app/store.ts b/config-ui/src/app/store.ts
index 8d2ce0c34..8fec5046c 100644
--- a/config-ui/src/app/store.ts
+++ b/config-ui/src/app/store.ts
@@ -19,10 +19,12 @@
 import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
 
 import { connectionsSlice } from '@/features';
+import { onboardSlice } from '@/features/onboard';
 
 export const store = configureStore({
   reducer: {
     connections: connectionsSlice.reducer,
+    onboard: onboardSlice.reducer,
   },
 });
 
diff --git a/config-ui/src/routes/onboard/context.tsx 
b/config-ui/src/features/onboard/index.ts
similarity index 55%
rename from config-ui/src/routes/onboard/context.tsx
rename to config-ui/src/features/onboard/index.ts
index e21d12d2b..513ab48a7 100644
--- a/config-ui/src/routes/onboard/context.tsx
+++ b/config-ui/src/features/onboard/index.ts
@@ -16,34 +16,4 @@
  *
  */
 
-import { createContext } from 'react';
-
-export type Record = {
-  plugin: string;
-  connectionId: ID;
-  blueprintId: ID;
-  pipelineId: ID;
-  scopeName: string;
-};
-
-const initialValue: {
-  step: number;
-  records: Record[];
-  done: boolean;
-  projectName?: string;
-  plugin?: string;
-  setStep: (value: number) => void;
-  setRecords: (value: Record[]) => void;
-  setProjectName: (value: string) => void;
-  setPlugin: (value: string) => void;
-} = {
-  step: 0,
-  records: [],
-  done: false,
-  setStep: () => {},
-  setRecords: () => {},
-  setProjectName: () => {},
-  setPlugin: () => {},
-};
-
-export const Context = createContext(initialValue);
+export * from './slice';
diff --git a/config-ui/src/features/onboard/slice.ts 
b/config-ui/src/features/onboard/slice.ts
new file mode 100644
index 000000000..1ddebf576
--- /dev/null
+++ b/config-ui/src/features/onboard/slice.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+
+import API from '@/api';
+import type { RootState } from '@/app/store';
+import type { IStatus } from '@/types';
+
+type DataType = {
+  initial: boolean;
+  step: number;
+  records: Array<{
+    plugin: string;
+    connectionId: ID;
+    blueprintId: ID;
+    pipelineId: ID;
+    scopeName: string;
+  }>;
+  projectName: string;
+  plugin: string;
+  done: boolean;
+};
+
+export const request = createAsyncThunk('onboard/request', async () => {
+  const res = await API.store.get('onboard');
+  return res;
+});
+
+export const update = createAsyncThunk('onboard/update', async (payload: 
Partial<DataType>, { getState }) => {
+  const { data } = (getState() as RootState).onboard;
+  const res = await API.store.set('onboard', {
+    ...data,
+    ...payload,
+    step: payload.step ?? data.step + 1,
+  });
+  return res;
+});
+
+export const done = createAsyncThunk('onboard/done', async (_, { getState }) 
=> {
+  const { data } = (getState() as RootState).onboard;
+  await API.store.set('onboard', {
+    ...data,
+    done: true,
+  });
+  return {};
+});
+
+const initialState: { status: IStatus; data: DataType } = {
+  status: 'idle',
+  data: {
+    initial: false,
+    step: 0,
+    records: [],
+    projectName: '',
+    plugin: '',
+    done: false,
+  },
+};
+
+export const onboardSlice = createSlice({
+  name: 'onboard',
+  initialState,
+  reducers: {
+    previous: (state) => {
+      state.data.step -= 1;
+    },
+    changeProjectName: (state, action) => {
+      state.data.projectName = action.payload;
+    },
+    changePlugin: (state, action) => {
+      state.data.plugin = action.payload;
+    },
+    changeRecords: (state, action) => {
+      state.data.records = action.payload;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      .addCase(request.pending, (state) => {
+        state.status = 'loading';
+      })
+      .addCase(request.fulfilled, (state, action) => {
+        state.status = 'success';
+        state.data = {
+          ...action.payload,
+          initial: action.payload?.initial ?? false,
+          step: action.payload?.step ?? 0,
+          records: action.payload?.records ?? [],
+          done: action.payload?.done ?? false,
+        };
+      })
+      .addCase(update.fulfilled, (state, action) => {
+        state.data = {
+          ...state.data,
+          ...action.payload,
+        };
+      })
+      .addCase(done.fulfilled, (state) => {
+        state.data.done = true;
+      });
+  },
+});
+
+export default onboardSlice.reducer;
+
+export const { previous, changeProjectName, changePlugin, changeRecords } = 
onboardSlice.actions;
+
+export const selectStatus = (state: RootState) => state.onboard.status;
+
+export const selectOnboard = (state: RootState) => state.onboard.data;
+
+export const selectRecord = (state: RootState) => {
+  const { plugin, records } = state.onboard.data;
+  return records.find((it) => it.plugin === plugin);
+};
diff --git a/config-ui/src/routes/app/app.tsx b/config-ui/src/routes/app/app.tsx
index 72a6b75aa..51c3ce0c7 100644
--- a/config-ui/src/routes/app/app.tsx
+++ b/config-ui/src/routes/app/app.tsx
@@ -19,8 +19,10 @@
 import { useEffect } from 'react';
 import { useNavigate, useLoaderData, Outlet } from 'react-router-dom';
 
+import { PageLoading } from '@/components';
 import { init } from '@/features';
-import { useAppDispatch } from '@/hooks';
+import { request as requestOnboard, selectStatus } from '@/features/onboard';
+import { useAppDispatch, useAppSelector } from '@/hooks';
 import { setUpRequestInterceptor } from '@/utils';
 
 export const App = () => {
@@ -29,11 +31,17 @@ export const App = () => {
   const { version, plugins } = useLoaderData() as { version: string; plugins: 
string[] };
 
   const dispatch = useAppDispatch();
+  const status = useAppSelector(selectStatus);
 
   useEffect(() => {
     setUpRequestInterceptor(navigate);
     dispatch(init({ version, plugins }));
+    dispatch(requestOnboard());
   }, []);
 
+  if (status === 'loading') {
+    return <PageLoading />;
+  }
+
   return <Outlet />;
 };
diff --git a/config-ui/src/routes/app/loader.ts 
b/config-ui/src/routes/app/loader.ts
index 51d532f6e..dad6d1267 100644
--- a/config-ui/src/routes/app/loader.ts
+++ b/config-ui/src/routes/app/loader.ts
@@ -16,7 +16,6 @@
  *
  */
 
-import { redirect } from 'react-router-dom';
 import { intersection } from 'lodash';
 
 import API from '@/api';
@@ -27,12 +26,6 @@ type Props = {
 };
 
 export const appLoader = async ({ request }: Props) => {
-  const onboard = await API.store.get('onboard');
-
-  if (!onboard) {
-    return redirect('/onboard');
-  }
-
   let fePlugins = getRegisterPlugins();
   const bePlugins = await API.plugin.list();
 
diff --git a/config-ui/src/routes/layout/layout.tsx 
b/config-ui/src/routes/layout/layout.tsx
index 8e45dd96e..a67036a5c 100644
--- a/config-ui/src/routes/layout/layout.tsx
+++ b/config-ui/src/routes/layout/layout.tsx
@@ -17,12 +17,13 @@
  */
 
 import { useState, useEffect, useMemo } from 'react';
-import { Outlet, useNavigate, useLocation } from 'react-router-dom';
+import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom';
 import { Helmet } from 'react-helmet';
 import { Layout as AntdLayout, Menu, Divider } from 'antd';
 
 import { PageLoading, Logo, ExternalLink } from '@/components';
-import { selectError, selectStatus, selectVersion } from '@/features';
+import { selectError, selectStatus, selectVersion } from 
'@/features/connections';
+import { selectOnboard } from '@/features/onboard';
 import { OnboardCard } from '@/routes/onboard/components';
 import { useAppSelector } from '@/hooks';
 
@@ -39,6 +40,7 @@ export const Layout = () => {
   const navigate = useNavigate();
   const { pathname } = useLocation();
 
+  const { initial } = useAppSelector(selectOnboard);
   const status = useAppSelector(selectStatus);
   const error = useAppSelector(selectError);
   const version = useAppSelector(selectVersion);
@@ -77,6 +79,10 @@ export const Layout = () => {
     throw error.message;
   }
 
+  if (!initial) {
+    return <Navigate to="/onboard" />;
+  }
+
   return (
     <AntdLayout style={{ height: '100%', overflow: 'hidden' }}>
       <Helmet>
diff --git a/config-ui/src/routes/onboard/components/card.tsx 
b/config-ui/src/routes/onboard/components/card.tsx
index 3c59ee082..b73786a29 100644
--- a/config-ui/src/routes/onboard/components/card.tsx
+++ b/config-ui/src/routes/onboard/components/card.tsx
@@ -16,14 +16,14 @@
  *
  */
 
-import { useState, useMemo } from 'react';
+import { useMemo } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { CloseOutlined, LoadingOutlined, CheckCircleFilled, CloseCircleFilled 
} from '@ant-design/icons';
 import { theme, Card, Flex, Progress, Space, Button, Modal } from 'antd';
 
 import API from '@/api';
-import { useRefreshData, useAutoRefresh } from '@/hooks';
-import { operator } from '@/utils';
+import { selectOnboard, selectRecord, done as doneFuc } from 
'@/features/onboard';
+import { useAppDispatch, useAppSelector, useAutoRefresh } from '@/hooks';
 
 import { DashboardURLMap } from '../step-4';
 
@@ -32,24 +32,21 @@ interface Props {
 }
 
 export const OnboardCard = ({ style }: Props) => {
-  const [oeprating, setOperating] = useState(false);
-  const [version, setVersion] = useState(0);
-
   const navigate = useNavigate();
 
+  const dispatch = useAppDispatch();
+  const { step, plugin, done } = useAppSelector(selectOnboard);
+  const record = useAppSelector(selectRecord);
+
   const {
     token: { green5, orange5, red5 },
   } = theme.useToken();
 
   const [modal, contextHolder] = Modal.useModal();
 
-  const { ready, data } = useRefreshData(() => API.store.get('onboard'), 
[version]);
-
-  const record = useMemo(() => (data ? data.records.find((it: any) => 
it.plugin === data.plugin) : null), [data]);
-
   const tasksRes = useAutoRefresh(
     async () => {
-      if ((data && data.done) || !record) {
+      if (done || !record) {
         return;
       }
 
@@ -64,7 +61,7 @@ export const OnboardCard = ({ style }: Props) => {
   );
 
   const status = useMemo(() => {
-    if (!data || data.step !== 4) {
+    if (step !== 4) {
       return 'prepare';
     }
 
@@ -83,30 +80,21 @@ export const OnboardCard = ({ style }: Props) => {
       default:
         return 'running';
     }
-  }, [data, tasksRes]);
+  }, [step, tasksRes]);
 
   const handleClose = async () => {
     modal.confirm({
       width: 600,
       title: 'Permanently close this entry?',
       content: 'You will not be able to get back to the onboarding session 
again.',
-      okButtonProps: {
-        loading: oeprating,
-      },
       okText: 'Confirm',
-      onOk: async () => {
-        const [success] = await operator(() => API.store.set('onboard', { 
...data, done: true }), {
-          setOperating,
-        });
-
-        if (success) {
-          setVersion(version + 1);
-        }
+      onOk() {
+        dispatch(doneFuc());
       },
     });
   };
 
-  if (!ready || !data || data.done) {
+  if (done) {
     return null;
   }
 
@@ -115,7 +103,7 @@ export const OnboardCard = ({ style }: Props) => {
       <Flex style={{ paddingRight: 50 }} align="center" 
justify="space-between">
         <Flex align="center">
           {status === 'prepare' && (
-            <Progress type="circle" size={30} format={() => `${data.step}/3`} 
percent={(data.step / 3) * 100} />
+            <Progress type="circle" size={30} format={() => `${step}/3`} 
percent={(step / 3) * 100} />
           )}
           {status === 'running' && <LoadingOutlined />}
           {status === 'success' && <CheckCircleFilled style={{ color: green5 
}} />}
@@ -157,7 +145,7 @@ export const OnboardCard = ({ style }: Props) => {
         )}
         {status === 'success' && (
           <Space>
-            <Button type="primary" onClick={() => 
window.open(DashboardURLMap[data.plugin])}>
+            <Button type="primary" onClick={() => 
window.open(DashboardURLMap[plugin])}>
               Check Dashboard
             </Button>
             <Button onClick={handleClose}>Finish</Button>
@@ -168,7 +156,7 @@ export const OnboardCard = ({ style }: Props) => {
             <Button type="primary" onClick={() => navigate('/onboard')}>
               Details
             </Button>
-            <Button onClick={() => 
window.open(DashboardURLMap[data.plugin])}>Check Dashboard</Button>
+            <Button onClick={() => window.open(DashboardURLMap[plugin])}>Check 
Dashboard</Button>
           </Space>
         )}
       </Flex>
diff --git a/config-ui/src/routes/onboard/components/tour.tsx 
b/config-ui/src/routes/onboard/components/tour.tsx
index 875ce2e18..bac65ce67 100644
--- a/config-ui/src/routes/onboard/components/tour.tsx
+++ b/config-ui/src/routes/onboard/components/tour.tsx
@@ -18,8 +18,8 @@
 
 import { Tour } from 'antd';
 
-import API from '@/api';
-import { useRefreshData } from '@/hooks';
+import { selectOnboard } from '@/features/onboard';
+import { useAppSelector } from '@/hooks';
 
 interface Props {
   nameRef: React.RefObject<HTMLInputElement>;
@@ -28,7 +28,7 @@ interface Props {
 }
 
 export const OnboardTour = ({ nameRef, connectionRef, configRef }: Props) => {
-  const { ready, data } = useRefreshData(() => API.store.get('onboard'), []);
+  const { step, done } = useAppSelector(selectOnboard);
 
   const steps = [
     {
@@ -49,7 +49,7 @@ export const OnboardTour = ({ nameRef, connectionRef, 
configRef }: Props) => {
     },
   ];
 
-  if (!ready || !data || data.step !== 4 || data.done) {
+  if (step !== 4 || done) {
     return null;
   }
 
diff --git a/config-ui/src/routes/onboard/index.tsx 
b/config-ui/src/routes/onboard/index.tsx
index 061be2033..239a4b973 100644
--- a/config-ui/src/routes/onboard/index.tsx
+++ b/config-ui/src/routes/onboard/index.tsx
@@ -16,19 +16,15 @@
  *
  */
 
-import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, Navigate } from 'react-router-dom';
 import { Helmet } from 'react-helmet';
 import { CloseOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
 import { theme, Layout, Modal } from 'antd';
 
-import API from '@/api';
-import { PageLoading } from '@/components';
+import { selectOnboard } from '@/features/onboard';
 import { PATHS } from '@/config';
-import { useRefreshData } from '@/hooks';
+import { useAppSelector } from '@/hooks';
 
-import type { Record } from './context';
-import { Context } from './context';
 import { Step0 } from './step-0';
 import { Step1 } from './step-1';
 import { Step2 } from './step-2';
@@ -59,30 +55,16 @@ interface Props {
 }
 
 export const Onboard = ({ logo, title }: Props) => {
-  const [step, setStep] = useState(0);
-  const [records, setRecords] = useState<Record[]>([]);
-  const [projectName, setProjectName] = useState<string>();
-  const [plugin, setPlugin] = useState<string>();
-
   const navigate = useNavigate();
 
+  const { step, done } = useAppSelector(selectOnboard);
+
   const {
     token: { colorPrimary },
   } = theme.useToken();
 
   const [modal, contextHolder] = Modal.useModal();
 
-  const { ready, data } = useRefreshData(() => API.store.get('onboard'));
-
-  useEffect(() => {
-    if (ready && data) {
-      setStep(data.step);
-      setRecords(data.records);
-      setProjectName(data.projectName);
-      setPlugin(data.plugin);
-    }
-  }, [ready, data]);
-
   const handleClose = () => {
     modal.confirm({
       width: 820,
@@ -94,58 +76,44 @@ export const Onboard = ({ logo, title }: Props) => {
     });
   };
 
-  if (!ready) {
-    return <PageLoading />;
+  if (done) {
+    return <Navigate to="/projects" />;
   }
 
   return (
-    <Context.Provider
-      value={{
-        step,
-        records,
-        done: false,
-        projectName,
-        plugin,
-        setStep,
-        setRecords,
-        setProjectName: setProjectName,
-        setPlugin: setPlugin,
-      }}
-    >
+    <Layout style={{ minHeight: '100vh' }}>
       <Helmet>
         <title>Onboard - {brandName}</title>
       </Helmet>
-      <Layout style={{ minHeight: '100vh' }}>
-        <S.Inner>
-          {step === 0 ? (
-            <Step0 logo={logo} title={title} />
-          ) : (
-            <>
-              <S.Header>
-                <h1>Connect to your first repository</h1>
-                <CloseOutlined style={{ fontSize: 18, color: '#70727F', 
cursor: 'pointer' }} onClick={handleClose} />
-              </S.Header>
-              <S.Content>
-                {[1, 2, 3].includes(step) && (
-                  <S.Step>
-                    {steps.map((it) => (
-                      <S.StepItem key={it.step} $actived={it.step === step} 
$activedColor={colorPrimary}>
-                        <span>{it.step}</span>
-                        <span>{it.title}</span>
-                      </S.StepItem>
-                    ))}
-                  </S.Step>
-                )}
-                {step === 1 && <Step1 />}
-                {step === 2 && <Step2 />}
-                {step === 3 && <Step3 />}
-                {step === 4 && <Step4 />}
-              </S.Content>
-            </>
-          )}
-        </S.Inner>
-        {contextHolder}
-      </Layout>
-    </Context.Provider>
+      <S.Inner>
+        {step === 0 ? (
+          <Step0 logo={logo} title={title} />
+        ) : (
+          <>
+            <S.Header>
+              <h1>Connect to your first repository</h1>
+              <CloseOutlined style={{ fontSize: 18, color: '#70727F', cursor: 
'pointer' }} onClick={handleClose} />
+            </S.Header>
+            <S.Content>
+              {[1, 2, 3].includes(step) && (
+                <S.Step>
+                  {steps.map((it) => (
+                    <S.StepItem key={it.step} $actived={it.step === step} 
$activedColor={colorPrimary}>
+                      <span>{it.step}</span>
+                      <span>{it.title}</span>
+                    </S.StepItem>
+                  ))}
+                </S.Step>
+              )}
+              {step === 1 && <Step1 />}
+              {step === 2 && <Step2 />}
+              {step === 3 && <Step3 />}
+              {step === 4 && <Step4 />}
+            </S.Content>
+          </>
+        )}
+      </S.Inner>
+      {contextHolder}
+    </Layout>
   );
 };
diff --git a/config-ui/src/routes/onboard/step-0.tsx 
b/config-ui/src/routes/onboard/step-0.tsx
index 5e504bd42..c015515f2 100644
--- a/config-ui/src/routes/onboard/step-0.tsx
+++ b/config-ui/src/routes/onboard/step-0.tsx
@@ -16,19 +16,17 @@
  *
  */
 
-import { useState, useContext } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { ExclamationCircleOutlined, CloseOutlined } from '@ant-design/icons';
 import { Modal, Flex, Button } from 'antd';
 import styled from 'styled-components';
 
-import API from '@/api';
 import { Logo } from '@/components';
 import { PATHS } from '@/config';
+import { update } from '@/features/onboard';
+import { useAppDispatch } from '@/hooks';
 import { operator } from '@/utils';
 
-import { Context } from './context';
-
 const Wrapper = styled.div`
   .logo {
     display: flex;
@@ -65,14 +63,11 @@ interface Props {
 }
 
 export const Step0 = ({ logo = <Logo direction="horizontal" />, title = 
'DevLake' }: Props) => {
-  const [operating, setOperating] = useState(false);
-
   const navigate = useNavigate();
+  const dispatch = useAppDispatch();
 
   const [modal, contextHolder] = Modal.useModal();
 
-  const { step, records, done, projectName, plugin, setStep } = 
useContext(Context);
-
   const handleClose = () => {
     modal.confirm({
       width: 820,
@@ -80,15 +75,10 @@ export const Step0 = ({ logo = <Logo direction="horizontal" 
/>, title = 'DevLake
       content: 'You can get back to this session via the card on top of the 
Projects page.',
       icon: <ExclamationCircleOutlined />,
       okText: 'Confirm',
-      onOk: async () => {
-        const [success] = await operator(
-          () => API.store.set('onboard', { step: 0, records, done, 
projectName, plugin }),
-          {
-            setOperating,
-            hideToast: true,
-          },
-        );
-
+      async onOk() {
+        const [success] = await operator(() => dispatch(update({ initial: 
true, step: 0 })).unwrap(), {
+          hideToast: true,
+        });
         if (success) {
           navigate(PATHS.ROOT());
         }
@@ -96,20 +86,6 @@ export const Step0 = ({ logo = <Logo direction="horizontal" 
/>, title = 'DevLake
     });
   };
 
-  const handleSubmit = async () => {
-    const [success] = await operator(
-      async () => API.store.set('onboard', { step: 1, records, done, 
projectName, plugin }),
-      {
-        setOperating,
-        hideToast: true,
-      },
-    );
-
-    if (success) {
-      setStep(step + 1);
-    }
-  };
-
   return (
     <Wrapper>
       {contextHolder}
@@ -123,7 +99,7 @@ export const Step0 = ({ logo = <Logo direction="horizontal" 
/>, title = 'DevLake
         </h1>
         <h4>With just a few clicks, you can integrate your initial DevOps tool 
and observe engineering metrics.</h4>
         <div className="action">
-          <Button block size="large" type="primary" loading={operating} 
onClick={handleSubmit}>
+          <Button block size="large" type="primary" onClick={() => 
dispatch(update({}))}>
             Connect to your first repository
           </Button>
         </div>
diff --git a/config-ui/src/routes/onboard/step-1.tsx 
b/config-ui/src/routes/onboard/step-1.tsx
index a6c16632a..a6582c446 100644
--- a/config-ui/src/routes/onboard/step-1.tsx
+++ b/config-ui/src/routes/onboard/step-1.tsx
@@ -16,24 +16,26 @@
  *
  */
 
-import { useState, useEffect, useContext } from 'react';
+import { useState, useEffect } from 'react';
 import { Link } from 'react-router-dom';
 import { Input, Flex, Button, message } from 'antd';
 
 import API from '@/api';
 import { Block, Markdown } from '@/components';
 import { PATHS } from '@/config';
+import { selectOnboard, update, previous, changeProjectName, changePlugin } 
from '@/features/onboard';
+import { useAppDispatch, useAppSelector } from '@/hooks';
 import { ConnectionSelect } from '@/plugins';
 import { operator } from '@/utils';
 
-import { Context } from './context';
 import * as S from './styled';
 
 export const Step1 = () => {
   const [QA, setQA] = useState('');
   const [operating, setOperating] = useState(false);
 
-  const { step, records, done, projectName, plugin, setStep, setProjectName, 
setPlugin } = useContext(Context);
+  const dispatch = useAppDispatch();
+  const { projectName, plugin } = useAppSelector(selectOnboard);
 
   useEffect(() => {
     fetch(`/onboard/step-1/${plugin ? plugin : 'default'}.md`)
@@ -56,14 +58,7 @@ export const Step1 = () => {
       return;
     }
 
-    const [success] = await operator(() => API.store.set('onboard', { step: 2, 
records, done, projectName, plugin }), {
-      setOperating,
-      hideToast: true,
-    });
-
-    if (success) {
-      setStep(step + 1);
-    }
+    dispatch(update({}));
   };
 
   return (
@@ -79,16 +74,16 @@ export const Step1 = () => {
               style={{ width: 386 }}
               placeholder="Your Project Name"
               value={projectName}
-              onChange={(e) => setProjectName(e.target.value)}
+              onChange={(e) => dispatch(changeProjectName(e.target.value))}
             />
           </Block>
           <Block
             title="Data Connection"
             description={
-              <p>
+              <>
                 For self-managed GitLab/GitHub/Bitbucket, please skip the 
onboarding and configure via{' '}
                 <Link to={PATHS.CONNECTIONS()}>Data Connections</Link>.
-              </p>
+              </>
             }
             required
           >
@@ -117,14 +112,14 @@ export const Step1 = () => {
                 },
               ]}
               value={plugin}
-              onChange={setPlugin}
+              onChange={(p) => dispatch(changePlugin(p))}
             />
           </Block>
         </div>
         <Markdown className="qa">{QA}</Markdown>
       </S.StepContent>
       <Flex style={{ marginTop: 64 }} justify="space-between">
-        <Button ghost type="primary" loading={operating} onClick={() => 
setStep(step - 1)}>
+        <Button ghost type="primary" loading={operating} onClick={() => 
dispatch(previous())}>
           Previous Step
         </Button>
         <Button type="primary" loading={operating} disabled={!projectName || 
!plugin} onClick={handleSubmit}>
diff --git a/config-ui/src/routes/onboard/step-2.tsx 
b/config-ui/src/routes/onboard/step-2.tsx
index cd2eba4dc..c306e026c 100644
--- a/config-ui/src/routes/onboard/step-2.tsx
+++ b/config-ui/src/routes/onboard/step-2.tsx
@@ -16,20 +16,21 @@
  *
  */
 
-import { useState, useContext, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 import { Link } from 'react-router-dom';
 import { Flex, Button, Tooltip } from 'antd';
 
 import API from '@/api';
 import { Markdown } from '@/components';
 import { PATHS } from '@/config';
+import { selectOnboard, previous, update } from '@/features/onboard';
+import { useAppDispatch, useAppSelector } from '@/hooks';
 import { getPluginConfig } from '@/plugins';
 import { ConnectionToken } from 
'@/plugins/components/connection-form/fields/token';
 import { ConnectionUsername } from 
'@/plugins/components/connection-form/fields/username';
 import { ConnectionPassword } from 
'@/plugins/components/connection-form/fields/password';
 import { operator } from '@/utils';
 
-import { Context } from './context';
 import * as S from './styled';
 
 const paramsMap: Record<string, any> = {
@@ -48,12 +49,12 @@ const paramsMap: Record<string, any> = {
 
 export const Step2 = () => {
   const [QA, setQA] = useState('');
-  const [operating, setOperating] = useState(false);
   const [testing, setTesting] = useState(false);
   const [testStaus, setTestStatus] = useState(false);
   const [payload, setPayload] = useState<any>({});
 
-  const { step, records, done, projectName, plugin, setStep, setRecords } = 
useContext(Context);
+  const dispatch = useAppDispatch();
+  const { plugin, records } = useAppSelector(selectOnboard);
 
   const config = useMemo(() => getPluginConfig(plugin as string), [plugin]);
 
@@ -91,38 +92,17 @@ export const Step2 = () => {
       return;
     }
 
-    const [success] = await operator(
-      async () => {
-        const connection = await API.connection.create(plugin, {
-          name: `${plugin}-${Date.now()}`,
-          ...paramsMap[plugin],
-          ...payload,
-        });
-
-        const newRecords = [
-          ...records,
-          { plugin, connectionId: connection.id, blueprintId: '', pipelineId: 
'', scopeName: '' },
-        ];
-
-        setRecords(newRecords);
-
-        await API.store.set('onboard', {
-          step: 3,
-          records: newRecords,
-          done,
-          projectName,
-          plugin,
-        });
-      },
-      {
-        setOperating,
-        hideToast: true,
-      },
-    );
+    const connection = await API.connection.create(plugin, {
+      name: `${plugin}-${Date.now()}`,
+      ...paramsMap[plugin],
+      ...payload,
+    });
 
-    if (success) {
-      setStep(step + 1);
-    }
+    dispatch(
+      update({
+        records: [...records, { plugin, connectionId: connection.id, 
blueprintId: '', pipelineId: '', scopeName: '' }],
+      }),
+    );
   };
 
   if (!plugin) {
@@ -205,10 +185,10 @@ export const Step2 = () => {
         <Markdown className="qa">{QA}</Markdown>
       </S.StepContent>
       <Flex style={{ marginTop: 36 }} justify="space-between">
-        <Button ghost type="primary" loading={operating} onClick={() => 
setStep(step - 1)}>
+        <Button ghost type="primary" onClick={() => dispatch(previous())}>
           Previous Step
         </Button>
-        <Button type="primary" loading={operating} disabled={!testStaus} 
onClick={handleSubmit}>
+        <Button type="primary" disabled={!testStaus} onClick={handleSubmit}>
           Next Step
         </Button>
       </Flex>
diff --git a/config-ui/src/routes/onboard/step-3.tsx 
b/config-ui/src/routes/onboard/step-3.tsx
index 4ee900bfa..58dcf85e9 100644
--- a/config-ui/src/routes/onboard/step-3.tsx
+++ b/config-ui/src/routes/onboard/step-3.tsx
@@ -16,16 +16,17 @@
  *
  */
 
-import { useState, useContext, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 import { Flex, Button } from 'antd';
 import dayjs from 'dayjs';
 
 import API from '@/api';
 import { Markdown } from '@/components';
+import { selectOnboard, previous, update } from '@/features/onboard';
+import { useAppDispatch, useAppSelector } from '@/hooks';
 import { DataScopeRemote, getPluginScopeId } from '@/plugins';
 import { operator, formatTime } from '@/utils';
 
-import { Context } from './context';
 import * as S from './styled';
 
 export const Step3 = () => {
@@ -33,7 +34,8 @@ export const Step3 = () => {
   const [operating, setOperating] = useState(false);
   const [scopes, setScopes] = useState<any[]>([]);
 
-  const { step, records, done, projectName, plugin, setStep, setRecords } = 
useContext(Context);
+  const dispatch = useAppDispatch();
+  const { projectName, plugin, records } = useAppSelector(selectOnboard);
 
   useEffect(() => {
     fetch(`/onboard/step-3/${plugin}.md`)
@@ -51,7 +53,7 @@ export const Step3 = () => {
       return;
     }
 
-    const [success] = await operator(
+    const [success, res] = await operator(
       async () => {
         // 1. create a new project
         const { blueprint } = await API.project.create({
@@ -89,7 +91,7 @@ export const Step3 = () => {
         // 5. get current run pipeline
         const pipeline = await API.blueprint.pipelines(blueprint.id);
 
-        const newRecords = records.map((it) =>
+        return records.map((it) =>
           it.plugin !== plugin
             ? it
             : {
@@ -99,17 +101,6 @@ export const Step3 = () => {
                 scopeName: scopes[0]?.fullName ?? scopes[0].name,
               },
         );
-
-        setRecords(newRecords);
-
-        // 6. update store
-        await API.store.set('onboard', {
-          step: 4,
-          records: newRecords,
-          done,
-          projectName,
-          plugin,
-        });
       },
       {
         setOperating,
@@ -118,7 +109,7 @@ export const Step3 = () => {
     );
 
     if (success) {
-      setStep(step + 1);
+      dispatch(update({ records: res }));
     }
   };
 
@@ -142,7 +133,7 @@ export const Step3 = () => {
         <Markdown className="qa">{QA}</Markdown>
       </S.StepContent>
       <Flex style={{ marginTop: 36 }} justify="space-between">
-        <Button ghost type="primary" loading={operating} onClick={() => 
setStep(step - 1)}>
+        <Button ghost type="primary" loading={operating} onClick={() => 
dispatch(previous())}>
           Previous Step
         </Button>
         <Button type="primary" loading={operating} disabled={!scopes.length} 
onClick={handleSubmit}>
diff --git a/config-ui/src/routes/onboard/step-4.tsx 
b/config-ui/src/routes/onboard/step-4.tsx
index eb1be9688..332e08b6b 100644
--- a/config-ui/src/routes/onboard/step-4.tsx
+++ b/config-ui/src/routes/onboard/step-4.tsx
@@ -16,19 +16,19 @@
  *
  */
 
-import { useState, useContext, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useState, useMemo } from 'react';
 import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
 import { theme, Progress, Space, Button } from 'antd';
 import styled from 'styled-components';
 
 import API from '@/api';
 import { ExternalLink } from '@/components';
+import { selectOnboard, selectRecord, update } from '@/features/onboard';
+import { useAppDispatch, useAppSelector } from '@/hooks';
 import { useAutoRefresh } from '@/hooks';
 import { operator } from '@/utils';
 
 import { Logs } from './components';
-import { Context } from './context';
 
 const Wrapper = styled.div`
   margin-top: 150px;
@@ -103,11 +103,9 @@ const getStatus = (data: any) => {
 export const Step4 = () => {
   const [operating, setOperating] = useState(false);
 
-  const navigate = useNavigate();
-
-  const { step, records, done, projectName, plugin, setRecords } = 
useContext(Context);
-
-  const record = useMemo(() => records.find((it) => it.plugin === plugin), 
[plugin, records]);
+  const dispatch = useAppDispatch();
+  const { plugin, records } = useAppSelector(selectOnboard);
+  const record = useAppSelector(selectRecord);
 
   const { data } = useAutoRefresh(
     async () => {
@@ -171,32 +169,12 @@ export const Step4 = () => {
     token: { green5, orange5, red5 },
   } = theme.useToken();
 
-  const handleFinish = async () => {
-    const [success] = await operator(
-      () =>
-        API.store.set('onboard', {
-          step,
-          records,
-          done: true,
-          projectName,
-          plugin,
-        }),
-      {
-        setOperating,
-      },
-    );
-
-    if (success) {
-      navigate('/');
-    }
-  };
-
   const handleRecollectData = async () => {
     if (!record) {
       return null;
     }
 
-    const [success] = await operator(
+    const [success, res] = await operator(
       async () => {
         // 1. re trigger this bulueprint
         await API.blueprint.trigger(record.blueprintId, { skipCollectors: 
false, fullSync: false });
@@ -204,7 +182,7 @@ export const Step4 = () => {
         // 2. get current run pipeline
         const pipeline = await API.blueprint.pipelines(record.blueprintId);
 
-        const newRecords = records.map((it) =>
+        return records.map((it) =>
           it.plugin !== plugin
             ? it
             : {
@@ -212,17 +190,6 @@ export const Step4 = () => {
                 pipelineId: pipeline.pipelines[0].id,
               },
         );
-
-        setRecords(newRecords);
-
-        // 3. update store
-        await API.store.set('onboard', {
-          step: 4,
-          records: newRecords,
-          done,
-          projectName,
-          plugin,
-        });
       },
       {
         setOperating,
@@ -230,6 +197,7 @@ export const Step4 = () => {
     );
 
     if (success) {
+      dispatch(update({ step: 4, records: res }));
     }
   };
 
@@ -261,7 +229,7 @@ export const Step4 = () => {
               <Button type="primary" onClick={() => 
window.open(DashboardURLMap[plugin])}>
                 Check Dashboard
               </Button>
-              <Button loading={operating} onClick={handleFinish}>
+              <Button loading={operating} onClick={() => dispatch(update({ 
step: 4, done: true }))}>
                 Finish and Exit
               </Button>
             </Space>


Reply via email to