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.