Skip to content

Commit

Permalink
feat: use redux rewrite project detail (#8095)
Browse files Browse the repository at this point in the history
  • Loading branch information
mintsweet authored Sep 24, 2024
1 parent 3367db4 commit 9251c42
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 47 deletions.
2 changes: 2 additions & 0 deletions config-ui/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand Down
19 changes: 19 additions & 0 deletions config-ui/src/features/project/index.ts
Original file line number Diff line number Diff line change
@@ -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';
65 changes: 65 additions & 0 deletions config-ui/src/features/project/slice.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 4 additions & 1 deletion config-ui/src/plugins/register/webhook/connection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
11 changes: 4 additions & 7 deletions config-ui/src/routes/project/additional-settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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+[ ]*)+)';
Expand All @@ -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) {
Expand Down Expand Up @@ -107,7 +105,6 @@ export const ProjectAdditionalSettings = () => {
);

if (success) {
setVersion((v) => v + 1);
navigate(`/projects/${encodeURIComponent(name)}`, {
state: {
tabId: 'settings',
Expand Down
15 changes: 5 additions & 10 deletions config-ui/src/routes/project/general-settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />;
Expand Down
18 changes: 16 additions & 2 deletions config-ui/src/routes/project/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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' }}>
Expand Down
61 changes: 36 additions & 25 deletions config-ui/src/routes/project/webhook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,107 +17,118 @@
*/

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';

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 = () => {
setType(undefined);
};

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,
},
],
};

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,
})),
],
};

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));
}
};

Expand All @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions config-ui/src/types/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

export interface IWebhookAPI {
id: number;
id: ID;
name: string;
postIssuesEndpoint: string;
closeIssuesEndpoint: string;
Expand All @@ -29,7 +29,7 @@ export interface IWebhookAPI {
}

export interface IWebhook {
id: number;
id: ID;
name: string;
postIssuesEndpoint: string;
closeIssuesEndpoint: string;
Expand Down

0 comments on commit 9251c42

Please sign in to comment.