feat: 完成了连接, 断开连接
update: 慢慢过渡到 css-in-js refactory: 新建连接改为 dialog wip: 没找到合适和适应的状态管理方便全局状态管理
This commit is contained in:
66
frontend/package-lock.json
generated
66
frontend/package-lock.json
generated
@ -10,9 +10,11 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.54.16",
|
||||
"@fluentui/react-icons": "^2.0.258",
|
||||
"jotai": "^2.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.2"
|
||||
"react-router-dom": "^6.26.2",
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
@ -3002,6 +3004,27 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/jotai": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/jotai/-/jotai-2.10.0.tgz",
|
||||
"integrity": "sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=17.0.0",
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -3502,6 +3525,35 @@
|
||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.0-rc.2",
|
||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@ -5549,6 +5601,12 @@
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"jotai": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/jotai/-/jotai-2.10.0.tgz",
|
||||
"integrity": "sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -5861,6 +5919,12 @@
|
||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
},
|
||||
"zustand": {
|
||||
"version": "5.0.0-rc.2",
|
||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,11 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.54.16",
|
||||
"@fluentui/react-icons": "^2.0.258",
|
||||
"jotai": "^2.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.2"
|
||||
"react-router-dom": "^6.26.2",
|
||||
"zustand": "^5.0.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
|
@ -1 +1 @@
|
||||
b35fc08c84ef0c2b0c3e1bf37916ac94
|
||||
f23304e575da740e9b738508a43df31e
|
@ -19,7 +19,7 @@ function isResp<T>(obj: any): obj is Resp<T> {
|
||||
);
|
||||
}
|
||||
|
||||
export async function Dial<T>(path: string, req: any = null): Promise<Resp<T>> {
|
||||
export async function Dial<T=any>(path: string, req: any = null): Promise<Resp<T>> {
|
||||
const bs = JSON.stringify(req)
|
||||
console.log(`[DEBUG] invoke req: path = ${path}, req =`, req)
|
||||
|
||||
|
136
frontend/src/component/connection/new.tsx
Normal file
136
frontend/src/component/connection/new.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import {
|
||||
DialogTrigger,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Button, Spinner, Field, Input, FieldProps, makeStyles, tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import {useState} from "react";
|
||||
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
|
||||
import {useToast} from "../../message";
|
||||
import {Dial} from "../../api";
|
||||
|
||||
const useActionStyle = makeStyles({
|
||||
container: {
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridColumnStart: 0,
|
||||
},
|
||||
test: {}
|
||||
});
|
||||
|
||||
interface ConnectionCreateProps {
|
||||
update: () => Promise<void>
|
||||
}
|
||||
|
||||
export function ConnectionCreate(props: ConnectionCreateProps){
|
||||
const actionStyle = useActionStyle();
|
||||
const {dispatchMessage} = useToast();
|
||||
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
|
||||
const buttonIcon =
|
||||
testLoading === "loading" ? (
|
||||
<Spinner size="tiny"/>
|
||||
) : testLoading === "success" ? (
|
||||
<CheckmarkFilled/>
|
||||
) : testLoading === "error" ? (
|
||||
<DismissRegular/>
|
||||
) : null;
|
||||
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
|
||||
name: '',
|
||||
endpoint: '',
|
||||
access: '',
|
||||
key: ''
|
||||
})
|
||||
|
||||
async function test() {
|
||||
setTestLoading("loading")
|
||||
let res = await Dial<string>("/api/connection/test", value)
|
||||
const status = res.status === 200 ? "success" : "error"
|
||||
setTestLoading(status);
|
||||
dispatchMessage(res.msg, status)
|
||||
}
|
||||
|
||||
async function create() {
|
||||
let res = await Dial("/api/connection/create", value)
|
||||
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
|
||||
if (res.status === 200) {
|
||||
await props.update()
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>新建S3连接</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className='connection-container'>
|
||||
<div className='connection-form'>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="name"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
>
|
||||
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
|
||||
onChange={(e) => {
|
||||
setValue({...value, name: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="endpoint"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
>
|
||||
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
|
||||
value={value.endpoint}
|
||||
onChange={(e) => {
|
||||
setValue({...value, endpoint: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret access"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
>
|
||||
<Input placeholder='' value={value.access} onChange={(e) => {
|
||||
setValue({...value, access: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret key"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
>
|
||||
<Input placeholder='' value={value.key} onChange={(e) => {
|
||||
setValue({...value, key: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions className={actionStyle.container}>
|
||||
<Button className={actionStyle.test} appearance='transparent' icon={buttonIcon}
|
||||
onClick={async () => await test()}>测试连接</Button>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="secondary">取消</Button>
|
||||
</DialogTrigger>
|
||||
<Button onClick={async () => {
|
||||
await create()
|
||||
}} appearance="primary">新建</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</>
|
||||
}
|
@ -17,11 +17,13 @@ div.body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.body div.body-connections {
|
||||
width: 200px;
|
||||
border-right: 1px solid lightgray;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.body-connections-search {
|
||||
@ -37,7 +39,8 @@ input.body-connections-search-input {
|
||||
outline: none;
|
||||
text-indent: 5px;
|
||||
}
|
||||
div.body-connections-search-dismiss{
|
||||
|
||||
div.body-connections-search-dismiss {
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
@ -49,13 +52,9 @@ div.body-connections-search-dismiss{
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.body-connections-list-item {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin: 1px 0;
|
||||
div.body-connections-list {
|
||||
height: 100%;
|
||||
}
|
||||
div.body-connections-list-item:first-child {
|
||||
margin-top: 8px;
|
||||
div.body-connections-list-item.active {
|
||||
color: var(--colorNeutralForeground2BrandSelected);
|
||||
}
|
158
frontend/src/component/home/home.tsx
Normal file
158
frontend/src/component/home/home.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import './home.css';
|
||||
import {
|
||||
Button, Dialog, DialogTrigger, makeStyles,mergeClasses, MenuItem, MenuList, tokens, Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
CloudAddFilled, DismissRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import {Dial} from "../../api";
|
||||
import {useToast} from "../../message";
|
||||
import {Connection} from "../../interfaces/connection";
|
||||
import {ConnectionCreate} from "../connection/new";
|
||||
|
||||
const useMenuListContainerStyles = makeStyles({
|
||||
container: {
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
fontSize: '15px',
|
||||
'& span': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'&.active': {
|
||||
color: tokens.colorNeutralForeground2BrandHover,
|
||||
}
|
||||
},
|
||||
item_icon: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
"&:hover" : {
|
||||
color: tokens.colorNeutralForeground2BrandHover,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function Home() {
|
||||
const styles = useMenuListContainerStyles();
|
||||
const {dispatchMessage} = useToast();
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [connectionFilterKeywords, setConnectionFilterKeywords] = useState<string>('');
|
||||
const [connections, setConnections] = useState<Connection[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
updateConnectionList().then()
|
||||
}, []);
|
||||
|
||||
async function updateConnectionList() {
|
||||
const res = await Dial<{ list: Connection[] }>("/api/connection/list");
|
||||
dispatchMessage(res.status === 200 ? '获取连接列表成功' : res.msg, res.status === 200 ? 'success' : 'error');
|
||||
setConnections(res.status === 200 ? res.data.list : connections);
|
||||
setOpenCreate(false)
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleConnect(item: Connection) {
|
||||
console.log('[DEBUG] db click item =', item)
|
||||
for (const c of connections) {
|
||||
if (item.id === c.id && c.active) {
|
||||
console.log('[DEBUG] conn is already connected:', c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let res = await Dial("/api/connection/connect", {id: item.id})
|
||||
if (res.status === 200) {
|
||||
dispatchMessage("连接成功", "success")
|
||||
setConnections(connections.map(one => {
|
||||
if (one.id === item.id) {
|
||||
one.active = true
|
||||
}
|
||||
|
||||
return one
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisconnect(item: Connection) {
|
||||
let res = await Dial('/api/connection/disconnect', {id: item.id})
|
||||
if (res.status === 200) {
|
||||
setConnections(connections.map(c => {
|
||||
if (item.id === c.id) {
|
||||
c.active = false
|
||||
}
|
||||
return c
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<Dialog
|
||||
open={openCreate}
|
||||
onOpenChange={(event, data) => setOpenCreate(data.open)}>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="primary" icon={<CloudAddFilled/>}>
|
||||
新建连接
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConnectionCreate update={updateConnectionList}/>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="body">
|
||||
<div className="body-connections">
|
||||
<div className="body-connections-search">
|
||||
<input className="body-connections-search-input" type={"text"} placeholder="搜索连接"
|
||||
value={connectionFilterKeywords}
|
||||
onChange={(e) => setConnectionFilterKeywords(e.target.value)}/>
|
||||
<div className="body-connections-search-dismiss" onClick={() => {
|
||||
setConnectionFilterKeywords('')
|
||||
}}>
|
||||
<DismissRegular/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-connections-list">
|
||||
<div className={styles.container}>
|
||||
<MenuList>
|
||||
{connections.map(item => {
|
||||
return <MenuItem
|
||||
onDoubleClick={async () => {
|
||||
await handleConnect(item)
|
||||
}}
|
||||
className={item.active ? mergeClasses(styles.item, 'active') : styles.item}
|
||||
key={item.id}>
|
||||
{item.name}
|
||||
<Tooltip content="断开连接" relationship="label">
|
||||
<Button onClick={async () => {await handleDisconnect(item)}} size="small" className={styles.item_icon} icon={<DismissRegular />} />
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
})}
|
||||
</MenuList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-content"></div>
|
||||
</div>
|
||||
<div className="footer"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
@ -6,4 +6,4 @@ export interface Connection {
|
||||
name: string;
|
||||
endpoint: string;
|
||||
active: boolean;
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import {createRoot} from 'react-dom/client'
|
||||
import './style.css'
|
||||
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||
import Home from "./page/home/home";
|
||||
import Connection from "./page/connection/connection";
|
||||
import Home from "./component/home/home";
|
||||
import {ToastProvider} from "./message";
|
||||
import {JotaiProvider} from "./store/store";
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
@ -13,7 +13,6 @@ const root = createRoot(container!)
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{path: '/', element: <Home/>},
|
||||
{path: '/connection', element: <Connection/>},
|
||||
])
|
||||
|
||||
root.render(
|
||||
|
@ -1,124 +0,0 @@
|
||||
import './connection.css'
|
||||
import {
|
||||
useId,
|
||||
Button,
|
||||
FieldProps,
|
||||
Spinner
|
||||
} from "@fluentui/react-components";
|
||||
import {Field, Input} from "@fluentui/react-components";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useState} from "react";
|
||||
import {Dial} from "../../api";
|
||||
import {useToast} from "../../message";
|
||||
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
|
||||
|
||||
|
||||
const Connection = (props: Partial<FieldProps>) => {
|
||||
const { dispatchMessage } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
|
||||
const buttonIcon =
|
||||
testLoading === "loading" ? (
|
||||
<Spinner size="tiny"/>
|
||||
) : testLoading === "success" ? (
|
||||
<CheckmarkFilled/>
|
||||
) : testLoading === "error" ? (
|
||||
<DismissRegular/>
|
||||
) : null;
|
||||
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
|
||||
name: '',
|
||||
endpoint: '',
|
||||
access: '',
|
||||
key: ''
|
||||
})
|
||||
|
||||
function test() {
|
||||
setTestLoading("loading")
|
||||
Dial<string>("/api/connection/test", value).then(res => {
|
||||
let status: "success" | "error" = "error"
|
||||
if (res.status === 200) {
|
||||
status = "success"
|
||||
}
|
||||
|
||||
setTestLoading(status);
|
||||
|
||||
dispatchMessage(res.msg, status)
|
||||
})
|
||||
}
|
||||
|
||||
function create() {
|
||||
Dial<unknown>("/api/connection/create", value).then(res => {
|
||||
dispatchMessage(res.msg, res.status === 200?"success":"error");
|
||||
if (res.status === 200) {
|
||||
navigate("/");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return <div className='connection-container'>
|
||||
<div className='connection-form'>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="name"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name} onChange={(e) => {
|
||||
setValue({...value, name: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="endpoint"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='地址 (example: https://ip_or_server-name:port)' value={value.endpoint}
|
||||
onChange={(e) => {
|
||||
setValue({...value, endpoint: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret access"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='' value={value.access} onChange={(e) => {
|
||||
setValue({...value, access: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field'>
|
||||
<Field
|
||||
label="secret key"
|
||||
validationState="success"
|
||||
validationMessage="This is a success message."
|
||||
{...props}
|
||||
>
|
||||
<Input placeholder='' value={value.key} onChange={(e) => {
|
||||
setValue({...value, key: e.target.value});
|
||||
}}/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='connection-form-field connection-form-field-actions'>
|
||||
<Button appearance='transparent' icon={buttonIcon} onClick={() => test()}>测试连接</Button>
|
||||
<div style={{marginLeft: 'auto'}}>
|
||||
<Button style={{marginRight: '20px'}} className='connection-form-field-actions-cancel'
|
||||
onClick={() => {
|
||||
navigate("/")
|
||||
}}>取消</Button>
|
||||
<Button className='connection-form-field-actions-confirm' appearance='primary'
|
||||
onClick={() => create()}>新建</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Connection;
|
@ -1,82 +0,0 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import './home.css';
|
||||
import {
|
||||
Button,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
CloudAddFilled, DismissRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Dial} from "../../api";
|
||||
import {useToast} from "../../message";
|
||||
import {Connection} from "../../interfaces/connection";
|
||||
|
||||
function Home() {
|
||||
const {dispatchMessage} = useToast();
|
||||
const [connectionFilterKeywords, setConnectionFilterKeywords] = useState<string>('');
|
||||
const navigate = useNavigate();
|
||||
const [connectionList, setConnectionList] = useState<Connection[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
Dial<{ list: Connection[] }>("/api/connection/list").then(res => {
|
||||
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
|
||||
if (res.status === 200) {
|
||||
setConnectionList(res.data.list)
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
async function handleConnect(item: Connection) {
|
||||
console.log('[DEBUG] double clicked item =', item)
|
||||
let res = await Dial<unknown>("/api/connection/connect", {id: item.id})
|
||||
if (res.status === 200) {
|
||||
connectionList.forEach((conn) => {
|
||||
if (conn.id === item.id) {
|
||||
conn.active = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<Button appearance="primary" icon={<CloudAddFilled/>} onClick={() => {
|
||||
navigate("/connection")
|
||||
}}>
|
||||
新建连接
|
||||
</Button>
|
||||
</div>
|
||||
<div className="body">
|
||||
<div className="body-connections">
|
||||
<div className="body-connections-search">
|
||||
<input className="body-connections-search-input" type={"text"} placeholder="搜索连接"
|
||||
value={connectionFilterKeywords}
|
||||
onChange={(e) => setConnectionFilterKeywords(e.target.value)}/>
|
||||
<div className="body-connections-search-dismiss" onClick={() => {
|
||||
setConnectionFilterKeywords('')
|
||||
}}>
|
||||
<DismissRegular/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-connections-list">
|
||||
{connectionList.map(item => {
|
||||
return <div className="body-connections-list-item" key={item.id}>
|
||||
<Button
|
||||
onDoubleClick={() => {
|
||||
handleConnect(item)
|
||||
}}
|
||||
appearance='transparent' style={{textIndent: '0px', justifyContent: 'left'}}
|
||||
>{item.name}</Button>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-content"></div>
|
||||
</div>
|
||||
<div className="footer"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
8
frontend/src/store/store.tsx
Normal file
8
frontend/src/store/store.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { Provider, createStore } from 'jotai'
|
||||
import {FC, ReactNode} from "react";
|
||||
|
||||
|
||||
export const JotaiProvider: FC<{ children: ReactNode }> = ({children}) => {
|
||||
const store = createStore();
|
||||
return <Provider store={store}>{children}</Provider>
|
||||
}
|
Reference in New Issue
Block a user