From 9251c42a7599d3b20aae1d0868a25024b3d7bb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E6=B9=9B?= <0x1304570@gmail.com> Date: Tue, 24 Sep 2024 21:09:56 +1200 Subject: [PATCH] feat: use redux rewrite project detail (#8095) --- config-ui/src/app/store.ts | 2 + config-ui/src/features/project/index.ts | 19 ++++++ config-ui/src/features/project/slice.ts | 65 +++++++++++++++++++ .../plugins/register/webhook/connection.tsx | 5 +- .../project/additional-settings/index.tsx | 11 ++-- .../routes/project/general-settings/index.tsx | 15 ++--- config-ui/src/routes/project/layout/index.tsx | 18 ++++- .../src/routes/project/webhook/index.tsx | 61 ++++++++++------- config-ui/src/types/webhook.ts | 4 +- 9 files changed, 153 insertions(+), 47 deletions(-) create mode 100644 config-ui/src/features/project/index.ts create mode 100644 config-ui/src/features/project/slice.ts diff --git a/config-ui/src/app/store.ts b/config-ui/src/app/store.ts index f95900a4635..5932edc7faa 100644 --- a/config-ui/src/app/store.ts +++ b/config-ui/src/app/store.ts @@ -21,12 +21,14 @@ import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; import { versionSlice } from '@/features/version'; import { connectionsSlice } from '@/features/connections'; import { onboardSlice } from '@/features/onboard'; +import { projectSlice } from '@/features/project'; export const store = configureStore({ reducer: { version: versionSlice.reducer, connections: connectionsSlice.reducer, onboard: onboardSlice.reducer, + project: projectSlice.reducer, }, }); diff --git a/config-ui/src/features/project/index.ts b/config-ui/src/features/project/index.ts new file mode 100644 index 00000000000..513ab48a7f8 --- /dev/null +++ b/config-ui/src/features/project/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + * + */ + +export * from './slice'; diff --git a/config-ui/src/features/project/slice.ts b/config-ui/src/features/project/slice.ts new file mode 100644 index 00000000000..0c59dba73e3 --- /dev/null +++ b/config-ui/src/features/project/slice.ts @@ -0,0 +1,65 @@ +/* + * 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 { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; + +import API from '@/api'; +import type { RootState } from '@/app/store'; +import type { IStatus, IProject } from '@/types'; + +export const request = createAsyncThunk('project/request', async (name: string) => { + const res = await API.project.get(name); + return res; +}); + +const initialState: { status: IStatus; data?: IProject } = { + status: 'loading', +}; + +export const projectSlice = createSlice({ + name: 'project', + initialState, + reducers: { + updateBlueprint: (state, action) => { + if (state.data) { + state.data.blueprint = action.payload; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(request.pending, (state) => { + state.status = 'loading'; + }) + .addCase(request.fulfilled, (state, action) => { + state.status = 'success'; + state.data = action.payload; + }) + .addCase(request.rejected, (state) => { + state.status = 'failed'; + }); + }, +}); + +export const { updateBlueprint } = projectSlice.actions; + +export default projectSlice.reducer; + +export const selectProjectStatus = (state: RootState) => state.project.status; + +export const selectProject = (state: RootState) => state.project.data; diff --git a/config-ui/src/plugins/register/webhook/connection.tsx b/config-ui/src/plugins/register/webhook/connection.tsx index 32bfd6f5e8b..c83f1813d13 100644 --- a/config-ui/src/plugins/register/webhook/connection.tsx +++ b/config-ui/src/plugins/register/webhook/connection.tsx @@ -115,7 +115,10 @@ export const WebHookConnection = ({ filterIds, fromProject = false, onAssociate, title="Remove this Webhook?" okText="Confirm" onCancel={handleHideDialog} - onOk={() => onRemove?.(currentID)} + onOk={() => { + onRemove?.(currentID); + handleHideDialog(); + }} > diff --git a/config-ui/src/routes/project/additional-settings/index.tsx b/config-ui/src/routes/project/additional-settings/index.tsx index 5d670bb772b..feac602150d 100644 --- a/config-ui/src/routes/project/additional-settings/index.tsx +++ b/config-ui/src/routes/project/additional-settings/index.tsx @@ -17,12 +17,13 @@ */ import { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Flex, Space, Card, Modal, Input, Checkbox, Button } from 'antd'; import API from '@/api'; import { Block, HelpTooltip, Message } from '@/components'; -import { useRefreshData } from '@/hooks'; +import { selectProject } from '@/features/project'; +import { useAppSelector } from '@/hooks'; import { operator } from '@/utils'; const RegexPrIssueDefaultValue = '(?mi)(Closes)[\\s]*.*(((and )?#\\d+[ ]*)+)'; @@ -41,13 +42,10 @@ export const ProjectAdditionalSettings = () => { }); const [operating, setOperating] = useState(false); const [open, setOpen] = useState(false); - const [version, setVersion] = useState(0); const navigate = useNavigate(); - const { pname } = useParams() as { pname: string }; - - const { data: project } = useRefreshData(() => API.project.get(pname), [pname, version]); + const project = useAppSelector(selectProject); useEffect(() => { if (!project) { @@ -107,7 +105,6 @@ export const ProjectAdditionalSettings = () => { ); if (success) { - setVersion((v) => v + 1); navigate(`/projects/${encodeURIComponent(name)}`, { state: { tabId: 'settings', diff --git a/config-ui/src/routes/project/general-settings/index.tsx b/config-ui/src/routes/project/general-settings/index.tsx index ef538afcc1b..4a7245a066e 100644 --- a/config-ui/src/routes/project/general-settings/index.tsx +++ b/config-ui/src/routes/project/general-settings/index.tsx @@ -16,20 +16,15 @@ * */ -import { useParams } from 'react-router-dom'; - -import API from '@/api'; -import { PageLoading } from '@/components'; -import { useRefreshData } from '@/hooks'; +import { selectProject } from '@/features/project'; +import { useAppSelector } from '@/hooks'; import { BlueprintDetail, FromEnum } from '@/routes'; export const ProjectGeneralSettings = () => { - const { pname } = useParams() as { pname: string }; - - const { ready, data: project } = useRefreshData(() => API.project.get(pname), [pname]); + const project = useAppSelector(selectProject); - if (!ready || !project) { - return ; + if (!project) { + return null; } return ; diff --git a/config-ui/src/routes/project/layout/index.tsx b/config-ui/src/routes/project/layout/index.tsx index d96747a297c..23d5e4dcac9 100644 --- a/config-ui/src/routes/project/layout/index.tsx +++ b/config-ui/src/routes/project/layout/index.tsx @@ -16,12 +16,14 @@ * */ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useParams, useNavigate, useLocation, Link, Outlet } from 'react-router-dom'; import { RollbackOutlined } from '@ant-design/icons'; import { Layout, Menu } from 'antd'; -import { PageHeader } from '@/components'; +import { PageHeader, PageLoading } from '@/components'; +import { request, selectProjectStatus, selectProject } from '@/features/project'; +import { useAppDispatch, useAppSelector } from '@/hooks'; import { ProjectSelector } from './project-selector'; import * as S from './styled'; @@ -86,6 +88,14 @@ export const ProjectLayout = () => { const navigate = useNavigate(); const { pathname } = useLocation(); + const dispatch = useAppDispatch(); + const status = useAppSelector(selectProjectStatus); + const project = useAppSelector(selectProject); + + useEffect(() => { + dispatch(request(pname)); + }, [pname]); + const { paths, selectedKeys, title } = useMemo(() => { const paths = pathname.split('/'); const key = paths.pop(); @@ -103,6 +113,10 @@ export const ProjectLayout = () => { }; }, [pathname]); + if (status === 'loading' || !project) { + return ; + } + return ( diff --git a/config-ui/src/routes/project/webhook/index.tsx b/config-ui/src/routes/project/webhook/index.tsx index 20e828f714f..3c9d937f3bd 100644 --- a/config-ui/src/routes/project/webhook/index.tsx +++ b/config-ui/src/routes/project/webhook/index.tsx @@ -17,13 +17,15 @@ */ import { useState, useMemo } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { PlusOutlined } from '@ant-design/icons'; import { Alert, Button } from 'antd'; import API from '@/api'; import { NoData } from '@/components'; -import { useRefreshData } from '@/hooks'; +import { selectProject, updateBlueprint } from '@/features/project'; +import { selectWebhooks } from '@/features/connections'; +import { useAppDispatch, useAppSelector } from '@/hooks'; import type { WebhookItemType } from '@/plugins/register/webhook'; import { WebhookCreateDialog, WebhookSelectorDialog, WebHookConnection } from '@/plugins/register/webhook'; import { operator } from '@/utils'; @@ -31,18 +33,20 @@ import { operator } from '@/utils'; export const ProjectWebhook = () => { const [type, setType] = useState<'selectExist' | 'create'>(); const [operating, setOperating] = useState(false); - const [version, setVersion] = useState(0); - const { pname } = useParams() as { pname: string }; - - const { data } = useRefreshData(() => API.project.get(pname), [pname, version]); + const dispatch = useAppDispatch(); + const project = useAppSelector(selectProject); + const webhooks = useAppSelector(selectWebhooks); const webhookIds = useMemo( () => - data?.blueprint - ? data?.blueprint.connections.filter((cs) => cs.pluginName === 'webhook').map((cs: any) => cs.connectionId) + project?.blueprint + ? project?.blueprint.connections + .filter((cs) => cs.pluginName === 'webhook') + .filter((cs) => webhooks.map((wh) => wh.id).includes(cs.connectionId)) + .map((cs: any) => cs.connectionId) : [], - [data], + [project], ); const handleCancel = () => { @@ -50,14 +54,16 @@ export const ProjectWebhook = () => { }; const handleCreate = async (id: ID) => { - if (!data) { + if (!project) { return; } const payload = { - ...data.blueprint, + ...project.blueprint, connections: [ - ...data.blueprint.connections, + ...project.blueprint.connections.filter( + (cs) => cs.pluginName !== 'webhook' || webhookIds.includes(cs.connectionId), + ), { pluginName: 'webhook', connectionId: id, @@ -65,25 +71,28 @@ export const ProjectWebhook = () => { ], }; - const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), { + const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), { setOperating, + hideToast: true, }); if (success) { - setVersion(version + 1); handleCancel(); + dispatch(updateBlueprint(payload)); } }; const handleSelect = async (items: WebhookItemType[]) => { - if (!data) { + if (!project) { return; } const payload = { - ...data.blueprint, + ...project.blueprint, connections: [ - ...data.blueprint.connections, + ...project.blueprint.connections.filter( + (cs) => cs.pluginName !== 'webhook' || webhookIds.includes(cs.connectionId), + ), ...items.map((it) => ({ pluginName: 'webhook', connectionId: it.id, @@ -91,33 +100,35 @@ export const ProjectWebhook = () => { ], }; - const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), { + const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), { setOperating, }); if (success) { - setVersion(version + 1); handleCancel(); + dispatch(updateBlueprint(payload)); } }; const handleDelete = async (id: ID) => { - if (!data) { + if (!project) { return; } const payload = { - ...data.blueprint, - connections: data.blueprint.connections.filter((cs) => !(cs.pluginName === 'webhook' && cs.connectionId === id)), + ...project.blueprint, + connections: project.blueprint.connections + .filter((cs) => cs.pluginName !== 'webhook' || webhookIds.includes(cs.connectionId)) + .filter((cs) => !(cs.pluginName === 'webhook' && cs.connectionId === id)), }; - const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), { + const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), { setOperating, }); if (success) { - setVersion(version + 1); handleCancel(); + dispatch(updateBlueprint(payload)); } }; @@ -135,7 +146,7 @@ export const ProjectWebhook = () => {
To calculate DORA after receiving Webhook data immediately, you can visit the{' '} - Status tab + Status tab {' '} of the Blueprint page and click on Run Now.
diff --git a/config-ui/src/types/webhook.ts b/config-ui/src/types/webhook.ts index 2035bf67595..5ea4b89cd17 100644 --- a/config-ui/src/types/webhook.ts +++ b/config-ui/src/types/webhook.ts @@ -17,7 +17,7 @@ */ export interface IWebhookAPI { - id: number; + id: ID; name: string; postIssuesEndpoint: string; closeIssuesEndpoint: string; @@ -29,7 +29,7 @@ export interface IWebhookAPI { } export interface IWebhook { - id: number; + id: ID; name: string; postIssuesEndpoint: string; closeIssuesEndpoint: string;