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 9251c42a7 feat: use redux rewrite project detail (#8095)
9251c42a7 is described below

commit 9251c42a7599d3b20aae1d0868a25024b3d7bb33
Author: 青湛 <0x1304...@gmail.com>
AuthorDate: Tue Sep 24 21:09:56 2024 +1200

    feat: use redux rewrite project detail (#8095)
---
 config-ui/src/app/store.ts                         |  2 +
 .../webhook.ts => features/project/index.ts}       | 21 +------
 config-ui/src/features/project/slice.ts            | 65 ++++++++++++++++++++++
 .../src/plugins/register/webhook/connection.tsx    |  5 +-
 .../routes/project/additional-settings/index.tsx   | 11 ++--
 .../src/routes/project/general-settings/index.tsx  | 15 ++---
 config-ui/src/routes/project/layout/index.tsx      | 18 +++++-
 config-ui/src/routes/project/webhook/index.tsx     | 61 +++++++++++---------
 config-ui/src/types/webhook.ts                     |  4 +-
 9 files changed, 135 insertions(+), 67 deletions(-)

diff --git a/config-ui/src/app/store.ts b/config-ui/src/app/store.ts
index f95900a46..5932edc7f 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/types/webhook.ts 
b/config-ui/src/features/project/index.ts
similarity index 66%
copy from config-ui/src/types/webhook.ts
copy to config-ui/src/features/project/index.ts
index 2035bf675..513ab48a7 100644
--- a/config-ui/src/types/webhook.ts
+++ b/config-ui/src/features/project/index.ts
@@ -16,23 +16,4 @@
  *
  */
 
-export interface IWebhookAPI {
-  id: number;
-  name: string;
-  postIssuesEndpoint: string;
-  closeIssuesEndpoint: string;
-  postPipelineDeployTaskEndpoint: string;
-  apiKey: {
-    id: number;
-    apiKey: string;
-  };
-}
-
-export interface IWebhook {
-  id: number;
-  name: string;
-  postIssuesEndpoint: string;
-  closeIssuesEndpoint: string;
-  postPipelineDeployTaskEndpoint: string;
-  apiKeyId: number;
-}
+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 000000000..0c59dba73
--- /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 32bfd6f5e..c83f1813d 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();
+          }}
         >
           <Message content="This will only remove the webhook from this 
project. To permanently delete the webhook, please visit the Connections page." 
/>
         </Modal>
diff --git a/config-ui/src/routes/project/additional-settings/index.tsx 
b/config-ui/src/routes/project/additional-settings/index.tsx
index 5d670bb77..feac60215 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 ef538afcc..4a7245a06 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 <PageLoading />;
+  if (!project) {
+    return null;
   }
 
   return <BlueprintDetail id={project.blueprint.id} from={FromEnum.project} />;
diff --git a/config-ui/src/routes/project/layout/index.tsx 
b/config-ui/src/routes/project/layout/index.tsx
index d96747a29..23d5e4dca 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 <PageLoading />;
+  }
+
   return (
     <Layout style={{ height: '100%', overflow: 'hidden' }}>
       <Sider width={240} style={{ padding: '36px 12px', backgroundColor: 
'#F9F9FA', borderRight: '1px solid #E7E9F3' }}>
diff --git a/config-ui/src/routes/project/webhook/index.tsx 
b/config-ui/src/routes/project/webhook/index.tsx
index 20e828f71..3c9d937f3 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 = () => {
             <div style={{ marginTop: 16 }}>
               To calculate DORA after receiving Webhook data immediately, you 
can visit the{' '}
               <b style={{ textDecoration: 'underline' }}>
-                <Link 
to={`/projects/${encodeURIComponent(pname)}/general-settings`}>Status tab</Link>
+                <Link to={`/projects/${encodeURIComponent(project?.name ?? 
'')}/general-settings`}>Status tab</Link>
               </b>{' '}
               of the Blueprint page and click on Run Now.
             </div>
diff --git a/config-ui/src/types/webhook.ts b/config-ui/src/types/webhook.ts
index 2035bf675..5ea4b89cd 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;

Reply via email to