You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by mi...@apache.org on 2024/09/07 01:40:22 UTC

(incubator-devlake) branch refactor-onboard created (now 9ebe9284d)

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

mintsweet pushed a change to branch refactor-onboard
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


      at 9ebe9284d refactor: use redux to rewrite onboard

This branch includes the following new commits:

     new 9ebe9284d refactor: use redux to rewrite onboard

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(incubator-devlake) 01/01: refactor: use redux to rewrite onboard

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 9ebe9284d01ebc3c61ccc1066a04715c4d4bfa2d
Author: mintsweet <0x...@gmail.com>
AuthorDate: Sat Sep 7 13:40:06 2024 +1200

    refactor: use redux to rewrite onboard
---
 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>