You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by mi...@apache.org on 2024/09/05 03:32:36 UTC
(incubator-devlake) 01/01: feat: use new project detail layout
This is an automated email from the ASF dual-hosted git repository.
mintsweet pushed a commit to branch feat-project-layout
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
commit 078e78ea299c800fa2b974e4cd3597bc5155e718
Author: mintsweet <0x...@gmail.com>
AuthorDate: Thu Sep 5 15:32:16 2024 +1200
feat: use new project detail layout
---
config-ui/src/api/project/index.ts | 2 +-
config-ui/src/app/routrer.tsx | 35 ++++--
config-ui/src/hooks/index.ts | 1 +
.../styled.ts => hooks/use-outside-click.ts} | 22 ++--
.../routes/blueprint/detail/blueprint-detail.tsx | 1 -
.../index.tsx} | 36 +++---
config-ui/src/routes/project/detail/index.tsx | 98 -----------------
.../styled.ts => general-settings/index.tsx} | 22 +++-
config-ui/src/routes/project/index.ts | 5 +-
config-ui/src/routes/project/layout/index.tsx | 102 +++++++++++++++++
.../src/routes/project/layout/project-selector.tsx | 122 +++++++++++++++++++++
.../routes/project/{detail => layout}/styled.ts | 52 ++++++++-
.../webhooks-panel.tsx => webhook/index.tsx} | 60 +++++-----
13 files changed, 393 insertions(+), 165 deletions(-)
diff --git a/config-ui/src/api/project/index.ts b/config-ui/src/api/project/index.ts
index 52e3d2351..6023ad8da 100644
--- a/config-ui/src/api/project/index.ts
+++ b/config-ui/src/api/project/index.ts
@@ -20,7 +20,7 @@ import type { IProject } from '@/types';
import { encodeName } from '@/routes';
import { request } from '@/utils';
-export const list = (data: Pagination): Promise<{ count: number; projects: IProject[] }> =>
+export const list = (data: Pagination & { keyword?: string }): Promise<{ count: number; projects: IProject[] }> =>
request('/projects', { data });
export const get = (name: string): Promise<IProject> => request(`/projects/${encodeName(name)}`);
diff --git a/config-ui/src/app/routrer.tsx b/config-ui/src/app/routrer.tsx
index ac3113db7..a11f599b9 100644
--- a/config-ui/src/app/routrer.tsx
+++ b/config-ui/src/app/routrer.tsx
@@ -27,7 +27,10 @@ import {
Connections,
Connection,
ProjectHomePage,
- ProjectDetailPage,
+ ProjectLayout,
+ ProjectGeneralSettings,
+ ProjectWebhook,
+ ProjectAdditionalSettings,
BlueprintHomePage,
BlueprintDetailPage,
BlueprintConnectionDetailPage,
@@ -52,6 +55,28 @@ export const router = createBrowserRouter([
path: `${PATH_PREFIX}/onboard`,
element: <Onboard />,
},
+ {
+ path: `${PATH_PREFIX}/projects/:pname`,
+ element: <ProjectLayout />,
+ children: [
+ {
+ index: true,
+ element: <Navigate to="general-settings" />,
+ },
+ {
+ path: 'general-settings',
+ element: <ProjectGeneralSettings />,
+ },
+ {
+ path: 'webhooks',
+ element: <ProjectWebhook />,
+ },
+ {
+ path: 'additional-settings',
+ element: <ProjectAdditionalSettings />,
+ },
+ ],
+ },
{
path: `${PATH_PREFIX}`,
element: <Layout />,
@@ -66,14 +91,6 @@ export const router = createBrowserRouter([
path: 'projects',
element: <ProjectHomePage />,
},
- {
- path: 'projects/:pname',
- element: <ProjectDetailPage />,
- },
- {
- path: 'projects/:pname/:unique',
- element: <BlueprintConnectionDetailPage />,
- },
{
path: 'connections',
element: <Connections />,
diff --git a/config-ui/src/hooks/index.ts b/config-ui/src/hooks/index.ts
index 076b27375..1d6200f14 100644
--- a/config-ui/src/hooks/index.ts
+++ b/config-ui/src/hooks/index.ts
@@ -18,5 +18,6 @@
export * from './extend-redux';
export * from './use-auto-refresh';
+export * from './use-outside-click';
export * from './use-refresh-data';
export * from './user-proxy-prefix';
diff --git a/config-ui/src/routes/project/detail/styled.ts b/config-ui/src/hooks/use-outside-click.ts
similarity index 60%
copy from config-ui/src/routes/project/detail/styled.ts
copy to config-ui/src/hooks/use-outside-click.ts
index f2ac11cfb..f1eb4fe7e 100644
--- a/config-ui/src/routes/project/detail/styled.ts
+++ b/config-ui/src/hooks/use-outside-click.ts
@@ -16,11 +16,19 @@
*
*/
-import styled from 'styled-components';
+import type { MutableRefObject } from 'react';
+import { useEffect } from 'react';
-export const Wrapper = styled.div``;
-
-export const DialogBody = styled.div`
- display: flex;
- align-items: center;
-`;
+export const useOutsideClick = (ref: MutableRefObject<HTMLDivElement | null>, cb: () => void) => {
+ useEffect(() => {
+ function handleClickOutside(event: any) {
+ if (ref.current && !ref.current.contains(event.target)) {
+ cb();
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [ref, cb]);
+};
diff --git a/config-ui/src/routes/blueprint/detail/blueprint-detail.tsx b/config-ui/src/routes/blueprint/detail/blueprint-detail.tsx
index 78a7b6e4f..b0b52cd11 100644
--- a/config-ui/src/routes/blueprint/detail/blueprint-detail.tsx
+++ b/config-ui/src/routes/blueprint/detail/blueprint-detail.tsx
@@ -67,7 +67,6 @@ export const BlueprintDetail = ({ id, from }: Props) => {
return (
<S.Wrapper>
<Tabs
- centered
items={[
{
key: 'status',
diff --git a/config-ui/src/routes/project/detail/settings-panel.tsx b/config-ui/src/routes/project/additional-settings/index.tsx
similarity index 92%
rename from config-ui/src/routes/project/detail/settings-panel.tsx
rename to config-ui/src/routes/project/additional-settings/index.tsx
index 5f3ce4048..11e72c074 100644
--- a/config-ui/src/routes/project/detail/settings-panel.tsx
+++ b/config-ui/src/routes/project/additional-settings/index.tsx
@@ -17,25 +17,18 @@
*/
import { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useParams, 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 { PATHS } from '@/config';
-import { IProject } from '@/types';
+import { useRefreshData } from '@/hooks';
import { operator } from '@/utils';
-import * as S from './styled';
-
const RegexPrIssueDefaultValue = '(?mi)(Closes)[\\s]*.*(((and )?#\\d+[ ]*)+)';
-interface Props {
- project: IProject;
- onRefresh: () => void;
-}
-
-export const SettingsPanel = ({ project, onRefresh }: Props) => {
+export const ProjectAdditionalSettings = () => {
const [name, setName] = useState('');
const [dora, setDora] = useState({
enable: false,
@@ -49,10 +42,19 @@ export const SettingsPanel = ({ project, onRefresh }: Props) => {
});
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]);
+
useEffect(() => {
+ if (!project) {
+ return;
+ }
+
const dora = project.metrics.find((ms) => ms.pluginName === 'dora');
const linker = project.metrics.find((ms) => ms.pluginName === 'linker');
const issueTrace = project.metrics.find((ms) => ms.pluginName === 'issue_trace');
@@ -71,6 +73,10 @@ export const SettingsPanel = ({ project, onRefresh }: Props) => {
}, [project]);
const handleUpdate = async () => {
+ if (!project) {
+ return;
+ }
+
const [success] = await operator(
() =>
API.project.update(project.name, {
@@ -102,7 +108,7 @@ export const SettingsPanel = ({ project, onRefresh }: Props) => {
);
if (success) {
- onRefresh();
+ setVersion((v) => v + 1);
navigate(PATHS.PROJECT(name), {
state: {
tabId: 'settings',
@@ -120,6 +126,10 @@ export const SettingsPanel = ({ project, onRefresh }: Props) => {
};
const handleDelete = async () => {
+ if (!project) {
+ return;
+ }
+
const [success] = await operator(() => API.project.remove(project.name), {
setOperating,
formatMessage: () => 'Delete project successful.',
@@ -215,9 +225,9 @@ export const SettingsPanel = ({ project, onRefresh }: Props) => {
onCancel={handleHideDeleteDialog}
onOk={handleDelete}
>
- <S.DialogBody>
+ <Flex align="center">
<Message content="This operation cannot be undone. Deleting a Data Connection will delete all data that have been collected in this Connection." />
- </S.DialogBody>
+ </Flex>
</Modal>
</Flex>
);
diff --git a/config-ui/src/routes/project/detail/index.tsx b/config-ui/src/routes/project/detail/index.tsx
deleted file mode 100644
index fb37a68eb..000000000
--- a/config-ui/src/routes/project/detail/index.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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 { useEffect, useState } from 'react';
-import { useParams, useLocation } from 'react-router-dom';
-import { Helmet } from 'react-helmet';
-import { Tabs } from 'antd';
-
-import API from '@/api';
-import { PageHeader, PageLoading } from '@/components';
-import { PATHS } from '@/config';
-import { useRefreshData } from '@/hooks';
-import { BlueprintDetail, FromEnum } from '@/routes';
-
-import { WebhooksPanel } from './webhooks-panel';
-import { SettingsPanel } from './settings-panel';
-import * as S from './styled';
-
-const brandName = import.meta.env.DEVLAKE_BRAND_NAME ?? 'DevLake';
-
-export const ProjectDetailPage = () => {
- const [version, setVersion] = useState(1);
- const [tabId, setTabId] = useState('blueprint');
-
- const { pname } = useParams() as { pname: string };
- const { state } = useLocation();
-
- useEffect(() => {
- setTabId(state?.tabId ?? 'blueprint');
- }, [state]);
-
- const { ready, data } = useRefreshData(() => API.project.get(pname), [pname, version]);
-
- const handleChangeTabId = (tabId: string) => {
- setTabId(tabId);
- };
-
- const handleRefresh = () => {
- setVersion((v) => v + 1);
- };
-
- if (!ready || !data) {
- return <PageLoading />;
- }
-
- return (
- <PageHeader
- breadcrumbs={[
- { name: 'Projects', path: PATHS.PROJECTS() },
- { name: data.name, path: PATHS.PROJECT(pname) },
- ]}
- >
- <Helmet>
- <title>
- {data.name} - {brandName}
- </title>
- </Helmet>
- <S.Wrapper>
- <Tabs
- items={[
- {
- key: 'blueprint',
- label: 'Blueprint',
- children: <BlueprintDetail id={data.blueprint.id} from={FromEnum.project} />,
- },
- {
- key: 'webhook',
- label: 'Webhooks',
- children: <WebhooksPanel project={data} onRefresh={handleRefresh} />,
- },
- {
- key: 'settings',
- label: 'Settings',
- children: <SettingsPanel project={data} onRefresh={handleRefresh} />,
- },
- ]}
- activeKey={tabId}
- onChange={handleChangeTabId}
- />
- </S.Wrapper>
- </PageHeader>
- );
-};
diff --git a/config-ui/src/routes/project/detail/styled.ts b/config-ui/src/routes/project/general-settings/index.tsx
similarity index 59%
copy from config-ui/src/routes/project/detail/styled.ts
copy to config-ui/src/routes/project/general-settings/index.tsx
index f2ac11cfb..ef538afcc 100644
--- a/config-ui/src/routes/project/detail/styled.ts
+++ b/config-ui/src/routes/project/general-settings/index.tsx
@@ -16,11 +16,21 @@
*
*/
-import styled from 'styled-components';
+import { useParams } from 'react-router-dom';
-export const Wrapper = styled.div``;
+import API from '@/api';
+import { PageLoading } from '@/components';
+import { useRefreshData } from '@/hooks';
+import { BlueprintDetail, FromEnum } from '@/routes';
-export const DialogBody = styled.div`
- display: flex;
- align-items: center;
-`;
+export const ProjectGeneralSettings = () => {
+ const { pname } = useParams() as { pname: string };
+
+ const { ready, data: project } = useRefreshData(() => API.project.get(pname), [pname]);
+
+ if (!ready || !project) {
+ return <PageLoading />;
+ }
+
+ return <BlueprintDetail id={project.blueprint.id} from={FromEnum.project} />;
+};
diff --git a/config-ui/src/routes/project/index.ts b/config-ui/src/routes/project/index.ts
index de11b8b83..59597a4c7 100644
--- a/config-ui/src/routes/project/index.ts
+++ b/config-ui/src/routes/project/index.ts
@@ -16,6 +16,9 @@
*
*/
+export * from './additional-settings';
export * from './utils';
export * from './home';
-export * from './detail';
+export * from './general-settings';
+export * from './layout';
+export * from './webhook';
diff --git a/config-ui/src/routes/project/layout/index.tsx b/config-ui/src/routes/project/layout/index.tsx
new file mode 100644
index 000000000..579fc16df
--- /dev/null
+++ b/config-ui/src/routes/project/layout/index.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 { useMemo } from 'react';
+import { useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';
+import { RollbackOutlined } from '@ant-design/icons';
+import { Layout, Menu } from 'antd';
+
+import { PageHeader } from '@/components';
+import { PATHS } from '@/config';
+
+import { ProjectSelector } from './project-selector';
+import * as S from './styled';
+
+const { Sider, Content } = Layout;
+
+const items = [
+ {
+ key: 'general-settings',
+ label: 'General Settings',
+ style: {
+ paddingLeft: 8,
+ },
+ },
+ {
+ key: 'webhooks',
+ label: 'Webhooks',
+ style: {
+ paddingLeft: 8,
+ },
+ },
+ {
+ key: 'additional-settings',
+ label: 'Additional Settings',
+ style: {
+ paddingLeft: 8,
+ },
+ },
+];
+
+export const ProjectLayout = () => {
+ const { pname } = useParams() as { pname: string };
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+
+ const { selectedKeys, breadcrumbs } = useMemo(() => {
+ const key = pathname.split('/').pop();
+ const item = items.find((i) => i.key === key);
+
+ return {
+ selectedKeys: key ? [key] : [],
+ breadcrumbs: [
+ {
+ name: item?.label ?? '',
+ path: '',
+ },
+ ],
+ };
+ }, [pathname]);
+
+ return (
+ <Layout style={{ height: '100%', overflow: 'hidden' }}>
+ <Sider width={240} style={{ padding: '36px 12px', backgroundColor: '#F9F9FA', borderRight: '1px solid #E7E9F3' }}>
+ <S.Top onClick={() => navigate(PATHS.PROJECTS())}>
+ <RollbackOutlined />
+ <span className="back">Back to Projects</span>
+ </S.Top>
+ <ProjectSelector name={pname} />
+ <Menu
+ mode="inline"
+ style={{ backgroundColor: '#F9F9FA', border: 'none' }}
+ items={items}
+ selectedKeys={selectedKeys}
+ onClick={({ key }) => navigate(`${PATHS.PROJECT(pname)}/${key}`)}
+ />
+ </Sider>
+ <Layout>
+ <Content style={{ padding: '36px 48px', overflowY: 'auto' }}>
+ <p>Configurations / Projects / {pname} /</p>
+ <PageHeader breadcrumbs={breadcrumbs}>
+ <Outlet />
+ </PageHeader>
+ </Content>
+ </Layout>
+ </Layout>
+ );
+};
diff --git a/config-ui/src/routes/project/layout/project-selector.tsx b/config-ui/src/routes/project/layout/project-selector.tsx
new file mode 100644
index 000000000..bcfffd615
--- /dev/null
+++ b/config-ui/src/routes/project/layout/project-selector.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { useState, useEffect, useReducer, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { CaretDownOutlined } from '@ant-design/icons';
+import { Input, Button, Flex } from 'antd';
+import { useDebounce } from 'ahooks';
+
+import { PageLoading } from '@/components';
+import { PATHS } from '@/config';
+import { useOutsideClick } from '@/hooks';
+import { operator } from '@/utils';
+
+import API from '@/api';
+
+import * as S from './styled';
+
+type StateType = { name: string }[];
+
+const reducer = (state: StateType, action: { type: string; payload: StateType }) => {
+ switch (action.type) {
+ case 'RESET':
+ return [...action.payload];
+ case 'APPEND':
+ return [...state, ...action.payload];
+ default:
+ return state;
+ }
+};
+
+interface Props {
+ name: string;
+}
+
+export const ProjectSelector = ({ name }: Props) => {
+ const [open, setOpen] = useState(false);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(1);
+ const [pageSize] = useState(10);
+ const [keyword, setKeyword] = useState('');
+ const [operating, setOperating] = useState(false);
+
+ const [state, dispatch] = useReducer(reducer, []);
+
+ const ref = useRef(null);
+
+ useOutsideClick(ref, () => setOpen(false));
+
+ const navigate = useNavigate();
+
+ const keywordDebounce = useDebounce(keyword, { wait: 500 });
+
+ useEffect(() => {
+ setOperating(true);
+ (async () => {
+ const res = await API.project.list({ page: 1, pageSize, keyword: keywordDebounce });
+ dispatch({ type: 'RESET', payload: res.projects });
+ setTotal(res.count);
+ setOperating(false);
+ })();
+ }, [keywordDebounce]);
+
+ const handleAppend = async () => {
+ const [success, res] = await operator(() => API.project.list({ page: page + 1, pageSize, keyword }), {
+ hideToast: true,
+ setOperating,
+ });
+
+ if (success) {
+ setPage(page + 1);
+ dispatch({ type: 'APPEND', payload: res.projects });
+ }
+ };
+
+ return (
+ <S.ProjectSelector ref={ref}>
+ <h1 onClick={() => setOpen(true)}>
+ <span>{name}</span>
+ <CaretDownOutlined style={{ fontSize: 12 }} />
+ </h1>
+ {open && (
+ <S.Selector>
+ <Input.Search placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
+ {operating ? (
+ <PageLoading />
+ ) : state.length ? (
+ <ul>
+ {state.map((it) => (
+ <li key={it.name} onClick={() => navigate(PATHS.PROJECT(it.name))}>
+ {it.name}
+ </li>
+ ))}
+ </ul>
+ ) : (
+ <p style={{ textAlign: 'center' }}>No Results</p>
+ )}
+ {total > state.length && (
+ <Flex style={{ marginTop: 16 }} align="center" justify="center" onClick={handleAppend}>
+ <Button loading={operating}>Load More</Button>
+ </Flex>
+ )}
+ </S.Selector>
+ )}
+ </S.ProjectSelector>
+ );
+};
diff --git a/config-ui/src/routes/project/detail/styled.ts b/config-ui/src/routes/project/layout/styled.ts
similarity index 50%
rename from config-ui/src/routes/project/detail/styled.ts
rename to config-ui/src/routes/project/layout/styled.ts
index f2ac11cfb..722e8453c 100644
--- a/config-ui/src/routes/project/detail/styled.ts
+++ b/config-ui/src/routes/project/layout/styled.ts
@@ -18,9 +18,53 @@
import styled from 'styled-components';
-export const Wrapper = styled.div``;
+export const Top = styled.div`
+ cursor: pointer;
-export const DialogBody = styled.div`
- display: flex;
- align-items: center;
+ & > span.back {
+ margin-left: 8px;
+ text-decoration: underline;
+ }
+`;
+
+export const ProjectSelector = styled.div`
+ position: relative;
+ padding: 16px 8px;
+
+ & > h1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ }
+`;
+
+export const Selector = styled.div`
+ position: absolute;
+ top: 50px;
+ right: 0;
+ left: 0;
+ padding: 16px 8px;
+ background-color: #fff;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ border-radius: 6px;
+ z-index: 1;
+
+ & > ul,
+ & > p {
+ margin-top: 16px;
+ }
+
+ & > ul > li {
+ display: flex;
+ padding: 8px 16px;
+ align-items: center;
+ min-height: 40px;
+ transition: background-color 0.3s;
+ cursor: pointer;
+
+ &:hover {
+ background-color: #f5f5f5;
+ }
+ }
`;
diff --git a/config-ui/src/routes/project/detail/webhooks-panel.tsx b/config-ui/src/routes/project/webhook/index.tsx
similarity index 80%
rename from config-ui/src/routes/project/detail/webhooks-panel.tsx
rename to config-ui/src/routes/project/webhook/index.tsx
index aefbbb7f2..fafd41d05 100644
--- a/config-ui/src/routes/project/detail/webhooks-panel.tsx
+++ b/config-ui/src/routes/project/webhook/index.tsx
@@ -17,32 +17,32 @@
*/
import { useState, useMemo } from 'react';
-import { Link } from 'react-router-dom';
+import { useParams, 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 type { WebhookItemType } from '@/plugins/register/webhook';
import { WebhookCreateDialog, WebhookSelectorDialog, WebHookConnection } from '@/plugins/register/webhook';
-import { IProject } from '@/types';
import { operator } from '@/utils';
-interface Props {
- project: IProject;
- onRefresh: () => void;
-}
-
-export const WebhooksPanel = ({ project, onRefresh }: Props) => {
+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 webhookIds = useMemo(
() =>
- project.blueprint
- ? project.blueprint.connections.filter((cs) => cs.pluginName === 'webhook').map((cs: any) => cs.connectionId)
+ data?.blueprint
+ ? data?.blueprint.connections.filter((cs) => cs.pluginName === 'webhook').map((cs: any) => cs.connectionId)
: [],
- [project],
+ [data],
);
const handleCancel = () => {
@@ -50,10 +50,14 @@ export const WebhooksPanel = ({ project, onRefresh }: Props) => {
};
const handleCreate = async (id: ID) => {
+ if (!data) {
+ return;
+ }
+
const payload = {
- ...project.blueprint,
+ ...data.blueprint,
connections: [
- ...project.blueprint.connections,
+ ...data.blueprint.connections,
{
pluginName: 'webhook',
connectionId: id,
@@ -61,20 +65,24 @@ export const WebhooksPanel = ({ project, onRefresh }: Props) => {
],
};
- const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), {
+ const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), {
setOperating,
});
if (success) {
- onRefresh();
+ setVersion(version + 1);
}
};
const handleSelect = async (items: WebhookItemType[]) => {
+ if (!data) {
+ return;
+ }
+
const payload = {
- ...project.blueprint,
+ ...data.blueprint,
connections: [
- ...project.blueprint.connections,
+ ...data.blueprint.connections,
...items.map((it) => ({
pluginName: 'webhook',
connectionId: it.id,
@@ -82,29 +90,31 @@ export const WebhooksPanel = ({ project, onRefresh }: Props) => {
],
};
- const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), {
+ const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), {
setOperating,
});
if (success) {
- onRefresh();
+ setVersion(version + 1);
}
};
const handleDelete = async (id: ID) => {
+ if (!data) {
+ return;
+ }
+
const payload = {
- ...project.blueprint,
- connections: project.blueprint.connections.filter(
- (cs) => !(cs.pluginName === 'webhook' && cs.connectionId === id),
- ),
+ ...data.blueprint,
+ connections: data.blueprint.connections.filter((cs) => !(cs.pluginName === 'webhook' && cs.connectionId === id)),
};
- const [success] = await operator(() => API.blueprint.update(project.blueprint.id, payload), {
+ const [success] = await operator(() => API.blueprint.update(data.blueprint.id, payload), {
setOperating,
});
if (success) {
- onRefresh();
+ setVersion(version + 1);
}
};