Compare commits
3 Commits
b2c13508f4
...
468759a437
Author | SHA1 | Date | |
---|---|---|---|
|
468759a437 | ||
|
bea30130f1 | ||
|
5e0885f22d |
66
frontend/package-lock.json
generated
66
frontend/package-lock.json
generated
@ -10,9 +10,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.54.16",
|
"@fluentui/react-components": "^9.54.16",
|
||||||
"@fluentui/react-icons": "^2.0.258",
|
"@fluentui/react-icons": "^2.0.258",
|
||||||
|
"jotai": "^2.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
@ -3002,6 +3004,27 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true
|
"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": {
|
"dependencies": {
|
||||||
@ -5549,6 +5601,12 @@
|
|||||||
"hasown": "^2.0.2"
|
"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": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true
|
"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": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.54.16",
|
"@fluentui/react-components": "^9.54.16",
|
||||||
"@fluentui/react-icons": "^2.0.258",
|
"@fluentui/react-icons": "^2.0.258",
|
||||||
|
"jotai": "^2.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@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)
|
const bs = JSON.stringify(req)
|
||||||
console.log(`[DEBUG] invoke req: path = ${path}, req =`, 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;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.body div.body-connections {
|
div.body div.body-connections {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
border-right: 1px solid lightgray;
|
border-right: 1px solid lightgray;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.body-connections-search {
|
div.body-connections-search {
|
||||||
@ -37,7 +39,8 @@ input.body-connections-search-input {
|
|||||||
outline: none;
|
outline: none;
|
||||||
text-indent: 5px;
|
text-indent: 5px;
|
||||||
}
|
}
|
||||||
div.body-connections-search-dismiss{
|
|
||||||
|
div.body-connections-search-dismiss {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -48,3 +51,10 @@ div.body-connections-search-dismiss{
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.body-connections-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
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
|
@ -3,9 +3,9 @@ import {createRoot} from 'react-dom/client'
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
import Home from "./page/home/home";
|
import Home from "./component/home/home";
|
||||||
import Connection from "./page/connection/connection";
|
|
||||||
import {ToastProvider} from "./message";
|
import {ToastProvider} from "./message";
|
||||||
|
import {JotaiProvider} from "./store/store";
|
||||||
|
|
||||||
const container = document.getElementById('root')
|
const container = document.getElementById('root')
|
||||||
|
|
||||||
@ -13,7 +13,6 @@ const root = createRoot(container!)
|
|||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{path: '/', element: <Home/>},
|
{path: '/', element: <Home/>},
|
||||||
{path: '/connection', element: <Connection/>},
|
|
||||||
])
|
])
|
||||||
|
|
||||||
root.render(
|
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,59 +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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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"></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>
|
||||||
|
}
|
3
frontend/wailsjs/go/controller/App.d.ts
vendored
3
frontend/wailsjs/go/controller/App.d.ts
vendored
@ -1,4 +1,7 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function Init(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
export function Invoke(arg1:string,arg2:string):Promise<string>;
|
export function Invoke(arg1:string,arg2:string):Promise<string>;
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function Init(arg1) {
|
||||||
|
return window['go']['controller']['App']['Init'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function Invoke(arg1, arg2) {
|
export function Invoke(arg1, arg2) {
|
||||||
return window['go']['controller']['App']['Invoke'](arg1, arg2);
|
return window['go']['controller']['App']['Invoke'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -14,6 +14,7 @@ require (
|
|||||||
github.com/loveuer/nf v0.2.11
|
github.com/loveuer/nf v0.2.11
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.18.4
|
github.com/ncruces/go-sqlite3/gormlite v0.18.4
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
|
github.com/samber/lo v1.38.1
|
||||||
github.com/wailsapp/wails/v2 v2.9.2
|
github.com/wailsapp/wails/v2 v2.9.2
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/postgres v1.5.9
|
gorm.io/driver/postgres v1.5.9
|
||||||
@ -60,7 +61,6 @@ require (
|
|||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/samber/lo v1.38.1 // indirect
|
|
||||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
@ -27,6 +27,9 @@ func Init(ctx context.Context) error {
|
|||||||
register("/api/connection/test", handler.ConnectionTest)
|
register("/api/connection/test", handler.ConnectionTest)
|
||||||
register("/api/connection/create", handler.ConnectionCreate)
|
register("/api/connection/create", handler.ConnectionCreate)
|
||||||
register("/api/connection/list", handler.ConnectionList)
|
register("/api/connection/list", handler.ConnectionList)
|
||||||
|
register("/api/connection/connect", handler.ConnectionConnect)
|
||||||
|
register("/api/connection/disconnect", handler.ConnectionDisconnect)
|
||||||
|
register("/api/connection/buckets", handler.ConnectionBuckets)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"github.com/loveuer/nf-disk/internal/api"
|
"github.com/loveuer/nf-disk/internal/api"
|
||||||
"github.com/loveuer/nf-disk/internal/db"
|
"github.com/loveuer/nf-disk/internal/db"
|
||||||
|
"github.com/loveuer/nf-disk/internal/manager"
|
||||||
"github.com/loveuer/nf-disk/internal/model"
|
"github.com/loveuer/nf-disk/internal/model"
|
||||||
"github.com/loveuer/nf-disk/internal/tool"
|
"github.com/loveuer/nf-disk/internal/tool"
|
||||||
"github.com/loveuer/nf-disk/ndh"
|
"github.com/loveuer/nf-disk/ndh"
|
||||||
@ -21,12 +22,17 @@ func NewApp() *App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Init(ctx context.Context) {
|
||||||
log.Info("app startup!!!")
|
log.Info("app init!!!")
|
||||||
|
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
|
||||||
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
|
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
|
||||||
tool.Must(model.Init(db.Default.Session()))
|
tool.Must(model.Init(db.Default.Session()))
|
||||||
|
tool.Must(manager.Init(ctx))
|
||||||
tool.Must(api.Init(ctx))
|
tool.Must(api.Init(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
log.Info("app startup!!!")
|
||||||
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/loveuer/nf-disk/internal/db"
|
"github.com/loveuer/nf-disk/internal/db"
|
||||||
"github.com/loveuer/nf-disk/internal/manager"
|
"github.com/loveuer/nf-disk/internal/manager"
|
||||||
"github.com/loveuer/nf-disk/internal/model"
|
"github.com/loveuer/nf-disk/internal/model"
|
||||||
"github.com/loveuer/nf-disk/internal/s3"
|
"github.com/loveuer/nf-disk/internal/s3"
|
||||||
"github.com/loveuer/nf-disk/ndh"
|
"github.com/loveuer/nf-disk/ndh"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConnectionTest(c *ndh.Ctx) error {
|
func ConnectionTest(c *ndh.Ctx) error {
|
||||||
@ -78,7 +80,7 @@ func ConnectionCreate(c *ndh.Ctx) error {
|
|||||||
return c.Send500(err.Error(), "创建连接失败(1)")
|
return c.Send500(err.Error(), "创建连接失败(1)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = manager.Register(connection, client); err != nil {
|
if err = manager.Manager.Register(connection, client); err != nil {
|
||||||
return c.Send500(err.Error(), "创建连接失败(2)")
|
return c.Send500(err.Error(), "创建连接失败(2)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,16 +88,126 @@ func ConnectionCreate(c *ndh.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ConnectionList(c *ndh.Ctx) error {
|
func ConnectionList(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
list = make([]*model.Connection, 0)
|
list = make([]*model.Connection, 0)
|
||||||
|
req = new(Req)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(nil, "参数错误")
|
||||||
|
}
|
||||||
|
|
||||||
if err = db.Default.Session().Model(&model.Connection{}).
|
if err = db.Default.Session().Model(&model.Connection{}).
|
||||||
Find(&list).
|
Find(&list).
|
||||||
Error; err != nil {
|
Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listMap := lo.SliceToMap(list, func(item *model.Connection) (uint64, *model.Connection) {
|
||||||
|
return item.Id, item
|
||||||
|
})
|
||||||
|
|
||||||
|
manager.Manager.Map(func(c *model.Connection, s *s3.Client) error {
|
||||||
|
if item, ok := listMap[c.Id]; ok {
|
||||||
|
item.Active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return c.Send200(map[string]any{"list": list})
|
return c.Send200(map[string]any{"list": list})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConnectionConnect(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = new(Req)
|
||||||
|
client *s3.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &model.Connection{Id: req.Id}
|
||||||
|
if err = conn.Get(db.Default.Session(), c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client, err = s3.New(c.Context(), conn.Endpoint, conn.Access, conn.Key); err != nil {
|
||||||
|
return c.Send500(err.Error(), "连接失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = manager.Manager.Register(conn, client); err != nil {
|
||||||
|
return c.Send500(err.Error(), "连接失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Send200(conn, "连接成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectionDisconnect(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = new(Req)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &model.Connection{Id: req.Id}
|
||||||
|
if err = conn.Get(db.Default.Session(), c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = manager.Manager.UnRegister(conn.Id); err != nil {
|
||||||
|
return c.Send500(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Send200(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectionBuckets(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = new(Req)
|
||||||
|
client *s3.Client
|
||||||
|
buckets []*s3.ListBucketRes
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(nil, "参数错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, client, err = manager.Manager.Use(req.Id); err != nil {
|
||||||
|
if errors.Is(err, manager.ErrNotFound) {
|
||||||
|
return c.Send400(nil, "所选连接未激活")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Send500(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if buckets, err = client.ListBucket(c.Context()); err != nil {
|
||||||
|
return c.Send500(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Send200(map[string]any{"list": buckets})
|
||||||
|
}
|
||||||
|
21
internal/handler/item.go
Normal file
21
internal/handler/item.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/loveuer/nf-disk/ndh"
|
||||||
|
|
||||||
|
func ListItem(c *ndh.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = new(Req)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.ReqParse(req); err != nil {
|
||||||
|
return c.Send400(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("implement me!!!")
|
||||||
|
}
|
7
internal/manager/error.go
Normal file
7
internal/manager/error.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
@ -5,14 +5,71 @@ import (
|
|||||||
"github.com/loveuer/nf-disk/internal/model"
|
"github.com/loveuer/nf-disk/internal/model"
|
||||||
"github.com/loveuer/nf-disk/internal/s3"
|
"github.com/loveuer/nf-disk/internal/s3"
|
||||||
"github.com/loveuer/nf/nft/log"
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
conn *model.Connection
|
||||||
|
client *s3.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type manager struct {
|
||||||
|
sync.Mutex
|
||||||
|
clients map[uint64]*client
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Manager *manager
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
return nil
|
Manager = &manager{
|
||||||
}
|
clients: make(map[uint64]*client),
|
||||||
|
}
|
||||||
func Register(m *model.Connection, c *s3.Client) error {
|
|
||||||
log.Debug("manager: register connection-client: id = %d, name = %s", m.Id, m.Name)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *manager) Register(c *model.Connection, s *s3.Client) error {
|
||||||
|
log.Debug("manager: register connection-client: id = %d, name = %s", c.Id, c.Name)
|
||||||
|
|
||||||
|
Manager.Lock()
|
||||||
|
defer Manager.Unlock()
|
||||||
|
Manager.clients[c.Id] = &client{conn: c, client: s}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) UnRegister(id uint64) error {
|
||||||
|
Manager.Lock()
|
||||||
|
defer Manager.Unlock()
|
||||||
|
c, ok := m.clients[id]
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("manager: register connection-client: id = %d, name = %s", c.conn, c.conn.Name)
|
||||||
|
|
||||||
|
delete(m.clients, id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) Map(fn func(*model.Connection, *s3.Client) error) error {
|
||||||
|
for _, item := range m.clients {
|
||||||
|
if err := fn(item.conn, item.client); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) Use(id uint64) (*model.Connection, *s3.Client, error) {
|
||||||
|
c, ok := m.clients[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.conn, c.client, nil
|
||||||
|
}
|
||||||
|
@ -1,9 +1,34 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"github.com/loveuer/nf-disk/internal/opt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
func Init(tx *gorm.DB) error {
|
func Init(tx *gorm.DB) (err error) {
|
||||||
return tx.AutoMigrate(
|
err = tx.AutoMigrate(
|
||||||
&Connection{},
|
&Connection{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if opt.Debug {
|
||||||
|
err = tx.Create([]*Connection{
|
||||||
|
{
|
||||||
|
Name: "dev-minio",
|
||||||
|
Endpoint: "http://10.220.10.15:9000",
|
||||||
|
Access: "8ALV3DUZI31YG4BDRJ0Z",
|
||||||
|
Key: "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
Endpoint: "http://10.220.10.14:19000",
|
||||||
|
Access: "5VCR05L4BSGNCTCD8DXP",
|
||||||
|
Key: "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u",
|
||||||
|
},
|
||||||
|
}).Clauses(clause.OnConflict{
|
||||||
|
DoNothing: true,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/loveuer/nf-disk/ndh"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||||
@ -11,8 +15,22 @@ type Connection struct {
|
|||||||
Endpoint string `json:"endpoint" gorm:"column:endpoint"`
|
Endpoint string `json:"endpoint" gorm:"column:endpoint"`
|
||||||
Access string `json:"access" gorm:"column:access"`
|
Access string `json:"access" gorm:"column:access"`
|
||||||
Key string `json:"key" gorm:"column:key"`
|
Key string `json:"key" gorm:"column:key"`
|
||||||
|
|
||||||
|
Active bool `json:"active" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) Create(tx *gorm.DB) error {
|
func (c *Connection) Create(tx *gorm.DB) error {
|
||||||
return tx.Create(c).Error
|
return tx.Create(c).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Get(tx *gorm.DB, ctx *ndh.Ctx) error {
|
||||||
|
if err := tx.Take(c, c.Id).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ctx.Send400(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Send500(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -7,5 +7,5 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Debug bool
|
Debug bool = false
|
||||||
)
|
)
|
||||||
|
37
internal/s3/list.go
Normal file
37
internal/s3/list.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListBucketRes struct {
|
||||||
|
CreatedAt int64
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListBucket(ctx context.Context) ([]*ListBucketRes, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
input = &s3.ListBucketsInput{
|
||||||
|
MaxBuckets: aws.Int32(100),
|
||||||
|
}
|
||||||
|
output *s3.ListBucketsOutput
|
||||||
|
)
|
||||||
|
|
||||||
|
if output, err = c.client.ListBuckets(ctx, input); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := lo.Map(
|
||||||
|
output.Buckets,
|
||||||
|
func(item types.Bucket, index int) *ListBucketRes {
|
||||||
|
return &ListBucketRes{CreatedAt: item.CreationDate.UnixMilli(), Name: *item.Name}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
17
main.go
17
main.go
@ -1,10 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/loveuer/nf-disk/internal/controller"
|
"github.com/loveuer/nf-disk/internal/controller"
|
||||||
"github.com/loveuer/nf/nft/nfctl/opt"
|
"github.com/loveuer/nf-disk/internal/opt"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
"github.com/loveuer/nf/nft/log"
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
@ -16,17 +19,23 @@ import (
|
|||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&opt.Debug, "debug", false, "debug mode")
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
flag.BoolVar(&opt.Debug, "debug", true, "debug mode")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if opt.Debug {
|
if opt.Debug {
|
||||||
log.SetLogLevel(log.LogLevelDebug)
|
log.SetLogLevel(log.LogLevelDebug)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := controller.NewApp()
|
app := controller.NewApp()
|
||||||
|
|
||||||
|
app.Init(ctx)
|
||||||
|
|
||||||
if err := wails.Run(&options.App{
|
if err := wails.Run(&options.App{
|
||||||
Title: "nf-disk",
|
Title: "nf-disk",
|
||||||
Width: 1024,
|
Width: 1024,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user