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 <[email protected]> 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>
