feat: website info and edit
This commit is contained in:
parent
6441230df4
commit
a4c4a6d9e9
@ -10,26 +10,28 @@ import { useUserStore } from './store/user';
|
|||||||
import { Register } from './pages/Register';
|
import { Register } from './pages/Register';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { queryClient } from './api/cache';
|
import { queryClient } from './api/cache';
|
||||||
function App() {
|
import { TokenLoginContainer } from './components/TokenLoginContainer';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const AppRoutes: React.FC = React.memo(() => {
|
||||||
const { info } = useUserStore();
|
const { info } = useUserStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{info && (
|
{info ? (
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/monitor" element={<Monitor />} />
|
<Route path="/monitor" element={<Monitor />} />
|
||||||
<Route path="/website" element={<Website />} />
|
<Route path="/website" element={<Website />} />
|
||||||
<Route path="/servers" element={<Servers />} />
|
<Route path="/servers" element={<Servers />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings/*" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
)}
|
) : (
|
||||||
|
<Route>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
@ -38,10 +40,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
AppRoutes.displayName = 'AppRoutes';
|
||||||
|
|
||||||
|
export const App: React.FC = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<TokenLoginContainer>
|
||||||
|
<AppRoutes />
|
||||||
|
</TokenLoginContainer>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
App.displayName = 'App';
|
||||||
export default App;
|
|
||||||
|
@ -26,6 +26,33 @@ export async function getWorkspaceWebsites(
|
|||||||
return data.websites;
|
return data.websites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkspaceWebsiteInfo(
|
||||||
|
workspaceId: string,
|
||||||
|
websiteId: string
|
||||||
|
): Promise<WebsiteInfo | null> {
|
||||||
|
const { data } = await request.get(`/api/workspace/website/${websiteId}`, {
|
||||||
|
params: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.website;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWorkspaceWebsiteInfo(
|
||||||
|
workspaceId: string,
|
||||||
|
websiteId: string,
|
||||||
|
info: { name: string; domain: string }
|
||||||
|
) {
|
||||||
|
await request.post(`/api/workspace/website/${websiteId}`, {
|
||||||
|
workspaceId,
|
||||||
|
name: info.name,
|
||||||
|
domain: info.domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.resetQueries(['websites', workspaceId]);
|
||||||
|
}
|
||||||
|
|
||||||
export function useWorspaceWebsites(workspaceId: string) {
|
export function useWorspaceWebsites(workspaceId: string) {
|
||||||
const { data: websites = [], isLoading } = useQuery(
|
const { data: websites = [], isLoading } = useQuery(
|
||||||
['websites', workspaceId],
|
['websites', workspaceId],
|
||||||
@ -37,6 +64,21 @@ export function useWorspaceWebsites(workspaceId: string) {
|
|||||||
return { websites, isLoading };
|
return { websites, isLoading };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useWorkspaceWebsiteInfo(
|
||||||
|
workspaceId: string,
|
||||||
|
websiteId: string
|
||||||
|
) {
|
||||||
|
const { data: website = null, isLoading } = useQuery(
|
||||||
|
['website', workspaceId, websiteId],
|
||||||
|
() => {
|
||||||
|
return getWorkspaceWebsiteInfo(workspaceId, websiteId);
|
||||||
|
},
|
||||||
|
{ cacheTime: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { website, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
export function refreshWorkspaceWebsites(workspaceId: string) {
|
export function refreshWorkspaceWebsites(workspaceId: string) {
|
||||||
queryClient.refetchQueries(['websites', workspaceId]);
|
queryClient.refetchQueries(['websites', workspaceId]);
|
||||||
}
|
}
|
||||||
|
6
src/client/components/ErrorTip.tsx
Normal file
6
src/client/components/ErrorTip.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ErrorTip: React.FC = React.memo(() => {
|
||||||
|
return <div>An unexpected error has occurred</div>;
|
||||||
|
});
|
||||||
|
ErrorTip.displayName = 'ErrorTip';
|
27
src/client/components/TokenLoginContainer.tsx
Normal file
27
src/client/components/TokenLoginContainer.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getJWT } from '../api/auth';
|
||||||
|
import { loginWithToken } from '../api/model/user';
|
||||||
|
import { Loading } from './Loading';
|
||||||
|
|
||||||
|
export const TokenLoginContainer: React.FC<React.PropsWithChildren> =
|
||||||
|
React.memo((props) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = getJWT();
|
||||||
|
if (token) {
|
||||||
|
loginWithToken().then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children}</>;
|
||||||
|
});
|
||||||
|
TokenLoginContainer.displayName = 'TokenLoginContainer';
|
87
src/client/components/WebsiteInfo.tsx
Normal file
87
src/client/components/WebsiteInfo.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Button, Form, Input, message } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import {
|
||||||
|
updateWorkspaceWebsiteInfo,
|
||||||
|
useWorkspaceWebsiteInfo,
|
||||||
|
} from '../api/model/website';
|
||||||
|
import { useRequest } from '../hooks/useRequest';
|
||||||
|
import { useCurrentWorkspaceId } from '../store/user';
|
||||||
|
import { ErrorTip } from './ErrorTip';
|
||||||
|
import { Loading } from './Loading';
|
||||||
|
import { NoWorkspaceTip } from './NoWorkspaceTip';
|
||||||
|
|
||||||
|
export const WebsiteInfo: React.FC = React.memo(() => {
|
||||||
|
const { websiteId } = useParams<{
|
||||||
|
websiteId: string;
|
||||||
|
}>();
|
||||||
|
const currentWorkspaceId = useCurrentWorkspaceId();
|
||||||
|
const { website, isLoading } = useWorkspaceWebsiteInfo(
|
||||||
|
currentWorkspaceId!,
|
||||||
|
websiteId!
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, handleSave] = useRequest(
|
||||||
|
async (values: { name: string; domain: string }) => {
|
||||||
|
await updateWorkspaceWebsiteInfo(currentWorkspaceId!, websiteId!, {
|
||||||
|
name: values.name,
|
||||||
|
domain: values.domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success('Save Success');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentWorkspaceId) {
|
||||||
|
return <NoWorkspaceTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
return <ErrorTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!website) {
|
||||||
|
return <ErrorTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="h-24 flex items-center">
|
||||||
|
<div className="text-2xl flex-1">Website Info</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
id: website.id,
|
||||||
|
name: website.name,
|
||||||
|
domain: website.domain,
|
||||||
|
}}
|
||||||
|
onFinish={handleSave}
|
||||||
|
>
|
||||||
|
<Form.Item label="Website ID" name="id">
|
||||||
|
<Input size="large" disabled={true} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Name" name="name" rules={[{ required: true }]}>
|
||||||
|
<Input size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Domain" name="domain" rules={[{ required: true }]}>
|
||||||
|
<Input size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button size="large" htmlType="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
WebsiteInfo.displayName = 'WebsiteInfo';
|
135
src/client/components/WebsiteList.tsx
Normal file
135
src/client/components/WebsiteList.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Form, Input, Modal, Table } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
addWorkspaceWebsite,
|
||||||
|
refreshWorkspaceWebsites,
|
||||||
|
useWorspaceWebsites,
|
||||||
|
WebsiteInfo,
|
||||||
|
} from '../api/model/website';
|
||||||
|
import { Loading } from './Loading';
|
||||||
|
import { NoWorkspaceTip } from './NoWorkspaceTip';
|
||||||
|
import { useRequest } from '../hooks/useRequest';
|
||||||
|
import { useUserStore } from '../store/user';
|
||||||
|
import { useEvent } from '../hooks/useEvent';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
export const WebsiteList: React.FC = React.memo(() => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const currentWorkspace = useUserStore(
|
||||||
|
(state) => state.info?.currentWorkspace
|
||||||
|
);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const [{ loading }, handleAddWebsite] = useRequest(async () => {
|
||||||
|
await form.validateFields();
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
|
||||||
|
await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain);
|
||||||
|
refreshWorkspaceWebsites(currentWorkspace!.id);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
|
||||||
|
form.resetFields();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
return <NoWorkspaceTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="h-24 flex items-center">
|
||||||
|
<div className="text-2xl flex-1">Websites</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
size="large"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
Add Website
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WebsiteListTable workspaceId={currentWorkspace.id} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Add Server"
|
||||||
|
open={isModalOpen}
|
||||||
|
okButtonProps={{
|
||||||
|
loading,
|
||||||
|
}}
|
||||||
|
onOk={() => handleAddWebsite()}
|
||||||
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
<Form layout="vertical" form={form}>
|
||||||
|
<Form.Item
|
||||||
|
label="Server Name"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Domain" name="domain" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
WebsiteList.displayName = 'WebsiteList';
|
||||||
|
|
||||||
|
const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
|
||||||
|
(props) => {
|
||||||
|
const { websites, isLoading } = useWorspaceWebsites(props.workspaceId);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleEdit = useEvent((websiteId) => {
|
||||||
|
console.log(`/settings/website/${websiteId}`);
|
||||||
|
navigate(`/settings/website/${websiteId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useMemo((): ColumnsType<WebsiteInfo> => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'domain',
|
||||||
|
title: 'Domain',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'action',
|
||||||
|
render: (record) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record.id)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button icon={<BarChartOutlined />}>View</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnsType<WebsiteInfo>;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Table columns={columns} dataSource={websites} pagination={false} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
WebsiteListTable.displayName = 'WebsiteListTable';
|
@ -3,7 +3,7 @@ import './index.css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import App from './App';
|
import { App } from './App';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Button, Form, Input, Typography } from 'antd';
|
import { Button, Form, Input, Typography } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { model } from '../api/model';
|
import { model } from '../api/model';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { loginWithToken } from '../api/model/user';
|
|
||||||
import { getJWT } from '../api/auth';
|
|
||||||
import { useRequest } from '../hooks/useRequest';
|
import { useRequest } from '../hooks/useRequest';
|
||||||
|
|
||||||
export const Login: React.FC = React.memo(() => {
|
export const Login: React.FC = React.memo(() => {
|
||||||
@ -14,15 +12,6 @@ export const Login: React.FC = React.memo(() => {
|
|||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = getJWT();
|
|
||||||
if (token) {
|
|
||||||
loginWithToken().then(() => {
|
|
||||||
navigate('/dashboard');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
<div className="w-80 -translate-y-1/4">
|
<div className="w-80 -translate-y-1/4">
|
||||||
|
@ -1,6 +1,49 @@
|
|||||||
|
import { Menu, MenuProps } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Routes, Route, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { WebsiteInfo } from '../components/WebsiteInfo';
|
||||||
|
import { WebsiteList } from '../components/WebsiteList';
|
||||||
|
import { useEvent } from '../hooks/useEvent';
|
||||||
|
|
||||||
export const Settings: React.FC = React.memo(() => {
|
export const Settings: React.FC = React.memo(() => {
|
||||||
return <div>Settings</div>;
|
const navigate = useNavigate();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: 'websites',
|
||||||
|
label: 'Websites',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onClick: MenuProps['onClick'] = useEvent((e) => {
|
||||||
|
navigate(`/settings/${e.key}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedKey =
|
||||||
|
(items.find((item) => pathname.startsWith(`/settings/${item?.key}`))
|
||||||
|
?.key as string) ?? 'websites';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full">
|
||||||
|
<div className="w-full md:w-1/6 pt-10">
|
||||||
|
<Menu
|
||||||
|
className="h-full"
|
||||||
|
onClick={onClick}
|
||||||
|
selectedKeys={[selectedKey]}
|
||||||
|
mode="vertical"
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
{pathname}
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-5/6 py-2 px-4">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<WebsiteList />} />
|
||||||
|
<Route path="/websites" element={<WebsiteList />} />
|
||||||
|
<Route path="/website/:websiteId" element={<WebsiteInfo />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
Settings.displayName = 'Settings';
|
Settings.displayName = 'Settings';
|
||||||
|
@ -1,120 +1,7 @@
|
|||||||
import {
|
import React from 'react';
|
||||||
BarChartOutlined,
|
import { WebsiteList } from '../components/WebsiteList';
|
||||||
EditOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Form, Input, Modal, Table } from 'antd';
|
|
||||||
import { ColumnsType } from 'antd/es/table';
|
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import {
|
|
||||||
addWorkspaceWebsite,
|
|
||||||
refreshWorkspaceWebsites,
|
|
||||||
useWorspaceWebsites,
|
|
||||||
WebsiteInfo,
|
|
||||||
} from '../api/model/website';
|
|
||||||
import { Loading } from '../components/Loading';
|
|
||||||
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
|
|
||||||
import { useRequest } from '../hooks/useRequest';
|
|
||||||
import { useUserStore } from '../store/user';
|
|
||||||
|
|
||||||
export const Website: React.FC = React.memo(() => {
|
export const Website: React.FC = React.memo(() => {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
return <WebsiteList />;
|
||||||
const currentWorkspace = useUserStore(
|
|
||||||
(state) => state.info?.currentWorkspace
|
|
||||||
);
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const [{ loading }, handleAddWebsite] = useRequest(async () => {
|
|
||||||
await form.validateFields();
|
|
||||||
const values = form.getFieldsValue();
|
|
||||||
|
|
||||||
await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain);
|
|
||||||
refreshWorkspaceWebsites(currentWorkspace!.id);
|
|
||||||
setIsModalOpen(false);
|
|
||||||
|
|
||||||
form.resetFields();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentWorkspace) {
|
|
||||||
return <NoWorkspaceTip />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="h-24 flex items-center">
|
|
||||||
<div className="text-2xl flex-1">Websites</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
size="large"
|
|
||||||
onClick={() => setIsModalOpen(true)}
|
|
||||||
>
|
|
||||||
Add Website
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WebsiteList workspaceId={currentWorkspace.id} />
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="Add Server"
|
|
||||||
open={isModalOpen}
|
|
||||||
okButtonProps={{
|
|
||||||
loading,
|
|
||||||
}}
|
|
||||||
onOk={() => handleAddWebsite()}
|
|
||||||
onCancel={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
<Form layout="vertical" form={form}>
|
|
||||||
<Form.Item
|
|
||||||
label="Server Name"
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Domain" name="domain" rules={[{ required: true }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
Website.displayName = 'Website';
|
Website.displayName = 'Website';
|
||||||
|
|
||||||
const WebsiteList: React.FC<{ workspaceId: string }> = React.memo((props) => {
|
|
||||||
const { websites, isLoading } = useWorspaceWebsites(props.workspaceId);
|
|
||||||
|
|
||||||
const columns = useMemo((): ColumnsType<WebsiteInfo> => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
dataIndex: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataIndex: 'domain',
|
|
||||||
title: 'Domain',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'action',
|
|
||||||
render: () => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button icon={<EditOutlined />}>Edit</Button>
|
|
||||||
<Button icon={<BarChartOutlined />}>View</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Table columns={columns} dataSource={websites} pagination={false} />;
|
|
||||||
});
|
|
||||||
WebsiteList.displayName = 'WebsiteList';
|
|
||||||
|
@ -22,3 +22,11 @@ export function setUserInfo(info: UserLoginInfo) {
|
|||||||
info,
|
info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCurrentWorkspaceId() {
|
||||||
|
const currentWorkspaceId = useUserStore(
|
||||||
|
(state) => state.info?.currentWorkspace?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentWorkspaceId;
|
||||||
|
}
|
||||||
|
@ -3,7 +3,8 @@ import { checkIsWorkspaceUser } from '../model/workspace';
|
|||||||
|
|
||||||
export function workspacePermission(): Handler {
|
export function workspacePermission(): Handler {
|
||||||
return async (req, res, next) => {
|
return async (req, res, next) => {
|
||||||
const workspaceId = req.body.workspaceId ?? req.query.workspaceId;
|
const workspaceId =
|
||||||
|
req.body.workspaceId ?? req.query.workspaceId ?? req.params.workspaceId;
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error('Cannot find workspace id');
|
throw new Error('Cannot find workspace id');
|
||||||
|
@ -35,6 +35,40 @@ export async function getWorkspaceWebsites(workspaceId: string) {
|
|||||||
return workspace?.websites ?? [];
|
return workspace?.websites ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkspaceWebsiteInfo(
|
||||||
|
workspaceId: string,
|
||||||
|
websiteId: string
|
||||||
|
) {
|
||||||
|
const websiteInfo = await prisma.website.findUnique({
|
||||||
|
where: {
|
||||||
|
id: websiteId,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return websiteInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWorkspaceWebsiteInfo(
|
||||||
|
workspaceId: string,
|
||||||
|
websiteId: string,
|
||||||
|
name: string,
|
||||||
|
domain: string
|
||||||
|
) {
|
||||||
|
const websiteInfo = await prisma.website.update({
|
||||||
|
where: {
|
||||||
|
id: websiteId,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
domain,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return websiteInfo;
|
||||||
|
}
|
||||||
|
|
||||||
export async function addWorkspaceWebsite(
|
export async function addWorkspaceWebsite(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { auth } from '../middleware/auth';
|
import { auth } from '../middleware/auth';
|
||||||
import { body, query, validate } from '../middleware/validate';
|
import { body, param, query, validate } from '../middleware/validate';
|
||||||
import { workspacePermission } from '../middleware/workspace';
|
import { workspacePermission } from '../middleware/workspace';
|
||||||
import { addWorkspaceWebsite, getWorkspaceWebsites } from '../model/workspace';
|
import {
|
||||||
|
addWorkspaceWebsite,
|
||||||
|
getWorkspaceWebsiteInfo,
|
||||||
|
getWorkspaceWebsites,
|
||||||
|
updateWorkspaceWebsiteInfo,
|
||||||
|
} from '../model/workspace';
|
||||||
|
|
||||||
export const workspaceRouter = Router();
|
export const workspaceRouter = Router();
|
||||||
|
|
||||||
@ -34,8 +39,16 @@ workspaceRouter.post(
|
|||||||
.withMessage('workspaceId should be string')
|
.withMessage('workspaceId should be string')
|
||||||
.isUUID()
|
.isUUID()
|
||||||
.withMessage('workspaceId should be UUID'),
|
.withMessage('workspaceId should be UUID'),
|
||||||
body('name').isString().withMessage('name should be a string'),
|
body('name')
|
||||||
body('domain').isURL().withMessage('domain should be URL')
|
.isString()
|
||||||
|
.withMessage('name should be string')
|
||||||
|
.isLength({ max: 100 })
|
||||||
|
.withMessage('length should be under 100'),
|
||||||
|
body('domain')
|
||||||
|
.isURL()
|
||||||
|
.withMessage('domain should be URL')
|
||||||
|
.isLength({ max: 500 })
|
||||||
|
.withMessage('length should be under 500')
|
||||||
),
|
),
|
||||||
auth(),
|
auth(),
|
||||||
workspacePermission(),
|
workspacePermission(),
|
||||||
@ -47,3 +60,71 @@ workspaceRouter.post(
|
|||||||
res.json({ website });
|
res.json({ website });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
workspaceRouter.get(
|
||||||
|
'/website/:websiteId',
|
||||||
|
validate(
|
||||||
|
query('workspaceId')
|
||||||
|
.isString()
|
||||||
|
.withMessage('workspaceId should be string')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('workspaceId should be UUID'),
|
||||||
|
param('websiteId')
|
||||||
|
.isString()
|
||||||
|
.withMessage('workspaceId should be string')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('workspaceId should be UUID')
|
||||||
|
),
|
||||||
|
auth(),
|
||||||
|
workspacePermission(),
|
||||||
|
async (req, res) => {
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const websiteId = req.params.websiteId;
|
||||||
|
|
||||||
|
const website = await getWorkspaceWebsiteInfo(workspaceId, websiteId);
|
||||||
|
|
||||||
|
res.json({ website });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceRouter.post(
|
||||||
|
'/website/:websiteId',
|
||||||
|
validate(
|
||||||
|
body('workspaceId')
|
||||||
|
.isString()
|
||||||
|
.withMessage('workspaceId should be string')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('workspaceId should be UUID'),
|
||||||
|
param('websiteId')
|
||||||
|
.isString()
|
||||||
|
.withMessage('workspaceId should be string')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('workspaceId should be UUID'),
|
||||||
|
body('name')
|
||||||
|
.isString()
|
||||||
|
.withMessage('name should be string')
|
||||||
|
.isLength({ max: 100 })
|
||||||
|
.withMessage('length should be under 100'),
|
||||||
|
body('domain')
|
||||||
|
.isURL()
|
||||||
|
.withMessage('domain should be URL')
|
||||||
|
.isLength({ max: 500 })
|
||||||
|
.withMessage('length should be under 500')
|
||||||
|
),
|
||||||
|
auth(),
|
||||||
|
workspacePermission(),
|
||||||
|
async (req, res) => {
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const websiteId = req.params.websiteId;
|
||||||
|
const { name, domain } = req.body;
|
||||||
|
|
||||||
|
const website = await updateWorkspaceWebsiteInfo(
|
||||||
|
workspaceId,
|
||||||
|
websiteId,
|
||||||
|
name,
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ website });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["./**/*"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user