mirror of
https://github.com/go-gost/gost-ui.git
synced 2024-08-11 17:53:29 +00:00
remote pro-components
This commit is contained in:
@@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
||||
public/monaco-editor
|
||||
Generated
+1844
-7140
File diff suppressed because it is too large
Load Diff
+9
-4
@@ -8,15 +8,19 @@
|
||||
"build": "tsc && vite build",
|
||||
"analyzer": "tsc && vite build --mode=analyzer",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"init:monaco": "cpx ./node_modules/monaco-editor/min/** ./public/monaco-editor/min",
|
||||
"postinstall": "npm run init:monaco",
|
||||
"predev": "npm run init:monaco",
|
||||
"prebuild": "npm run init:monaco",
|
||||
"preanalyzer": "npm run init:monaco"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/pro-components": "^2.6.35",
|
||||
"antd": "^5.11.0",
|
||||
"axios": "^1.6.0",
|
||||
"classnames": "^2.5.1",
|
||||
"events": "^3.3.0",
|
||||
"i": "^0.3.7",
|
||||
"idb": "^7.1.1",
|
||||
"idb-open-plus": "^1.0.0",
|
||||
"jsonc": "^2.0.0",
|
||||
@@ -38,11 +42,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"cpx2": "^7.0.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-bundle-analyzer": "^0.4.0"
|
||||
"vite-bundle-analyzer": "^0.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -1,3 +1,12 @@
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -13,3 +22,28 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.antd-cord-boxShadow {
|
||||
box-shadow: 0 1px 2px -2px #00000029,0 3px 6px #0000001f,0 5px 12px 4px #00000017;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.home-form {
|
||||
width: 380px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.home-form h1 {
|
||||
text-align: center;
|
||||
font-size: 30px;
|
||||
margin: .5em 0 0 0;
|
||||
}
|
||||
.home-form h2 {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
padding: 0 0 1em 0;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
+7
-4
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { ConfigProvider, message, theme } from "antd";
|
||||
import {
|
||||
init,
|
||||
@@ -13,10 +13,11 @@ import "./App.css";
|
||||
import { configEvent } from "./uitls/events";
|
||||
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import Manage from "./Pages/Manage";
|
||||
import { ServerComm } from "./api/local";
|
||||
import Ctx from "./uitls/ctx";
|
||||
|
||||
const Manage = React.lazy(() => import("./Pages/Manage"));
|
||||
|
||||
function App() {
|
||||
const info = useInfo();
|
||||
// const [gostConfig, setGostConfig] = useState<any>(null);
|
||||
@@ -44,7 +45,7 @@ function App() {
|
||||
]);
|
||||
useServerConfig.set(l1 as any);
|
||||
useLocalConfig.set(l2);
|
||||
return [l1, l2];
|
||||
return [l1, l2];
|
||||
} finally {
|
||||
setServerLoading(false);
|
||||
setLocalLoading(false);
|
||||
@@ -123,7 +124,9 @@ function App() {
|
||||
theme={{ algorithm: isDark ? theme.darkAlgorithm : undefined }}
|
||||
locale={zhCN}
|
||||
>
|
||||
{info ? <Manage /> : <Home />}
|
||||
<React.Suspense fallback="loading...">
|
||||
{info ? <Manage /> : <Home />}
|
||||
</React.Suspense>
|
||||
</ConfigProvider>
|
||||
</Ctx.Provider>
|
||||
);
|
||||
|
||||
+54
-145
@@ -1,132 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
BetaSchemaForm,
|
||||
ProForm,
|
||||
LoginForm,
|
||||
ProFormText,
|
||||
ProFormCheckbox,
|
||||
} from "@ant-design/pro-components";
|
||||
import type {
|
||||
ProFormColumnsType,
|
||||
ProFormLayoutType,
|
||||
} from "@ant-design/pro-components";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
GlobalOutlined,
|
||||
LockOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Checkbox, Col, Flex, Form, Row, Space } from "antd";
|
||||
import {
|
||||
GostApiConfig,
|
||||
getLocalServers,
|
||||
login,
|
||||
deleteLocal,
|
||||
} from "../uitls/server";
|
||||
|
||||
type DataItem = { name: string; state: string };
|
||||
const columns: ProFormColumnsType<DataItem>[] = [
|
||||
{
|
||||
title: "gost API 地址",
|
||||
dataIndex: "addr",
|
||||
valueType: "text",
|
||||
},
|
||||
{
|
||||
title: "usrname",
|
||||
dataIndex: "addr",
|
||||
valueType: "text",
|
||||
},
|
||||
{
|
||||
title: "password",
|
||||
dataIndex: "addr",
|
||||
valueType: "password",
|
||||
},
|
||||
];
|
||||
|
||||
const LocalServers = () => {
|
||||
const [list, setList] = useState<GostApiConfig[]>();
|
||||
// const [local, setLocal] = useState<Record<string, GostApiConfig>>();
|
||||
|
||||
const updateList = useCallback(async () => {
|
||||
return getLocalServers()
|
||||
.then((local) => {
|
||||
|
||||
return local
|
||||
.sort((a, b) => {
|
||||
const t1 = a.time || 0;
|
||||
const t2 = b.time || 0;
|
||||
return t2 - t1;
|
||||
});
|
||||
})
|
||||
.then((list) => setList(list));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
updateList();
|
||||
// getList().then(list=>setList(list))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && list?.length > 0 ? (
|
||||
<Space direction="vertical" style={{ display: "flex" }}>
|
||||
<div>快速连接</div>
|
||||
<Row gutter={10}>
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<Col
|
||||
key={item.addr}
|
||||
span={12}
|
||||
title={item.addr}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<Flex gap={5} style={{ overflow: "hidden" }}>
|
||||
<a
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
flex: "auto",
|
||||
}}
|
||||
href={`?use=${item.addr}`}
|
||||
>
|
||||
{item.addr}
|
||||
</a>
|
||||
<DeleteOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={async () => {
|
||||
await deleteLocal(item.addr);
|
||||
updateList();
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Space>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
import React from "react";
|
||||
import { GlobalOutlined, LockOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { Button, Checkbox, Form, Input } from "antd";
|
||||
import { login } from "../uitls/server";
|
||||
import LocalServers from "../components/LocalServers";
|
||||
|
||||
const Home: React.FC = () => {
|
||||
return (
|
||||
<LoginForm
|
||||
containerStyle={{ boxSizing: "border-box" }}
|
||||
title="GOST API Manage"
|
||||
subTitle="首先连接API服务"
|
||||
<Form
|
||||
className="home-form"
|
||||
size="large"
|
||||
layout="horizontal"
|
||||
submitter={{
|
||||
searchConfig: { submitText: "连接" },
|
||||
initialValues={{
|
||||
baseURL: "http://",
|
||||
save: true,
|
||||
}}
|
||||
onFinish={(value) => {
|
||||
let addr: string = value.baseURL;
|
||||
if (!/^(https?:)?\/\//.test(addr)) {
|
||||
addr = `${location.protocol}//` + addr;
|
||||
}else if(/^\/\//.test(addr)){
|
||||
} else if (/^\/\//.test(addr)) {
|
||||
addr = `${location.protocol}` + addr;
|
||||
}
|
||||
return login(
|
||||
@@ -140,40 +33,56 @@ const Home: React.FC = () => {
|
||||
value.save
|
||||
);
|
||||
}}
|
||||
actions={<LocalServers></LocalServers>}
|
||||
>
|
||||
<ProFormText
|
||||
<h1>GOST API Manage</h1>
|
||||
<h2>首先连接API服务</h2>
|
||||
<Form.Item
|
||||
name="baseURL"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <GlobalOutlined className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"API baseURL"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入API地址",
|
||||
},
|
||||
{
|
||||
validator(rule, value, callback) {
|
||||
if (value === "http://") {
|
||||
callback("请输入API地址");
|
||||
}
|
||||
callback();
|
||||
},
|
||||
},
|
||||
// {
|
||||
// type:'url'
|
||||
// }
|
||||
]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <UserOutlined className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"username"}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <LockOutlined className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"password"}
|
||||
/>
|
||||
<ProFormCheckbox labelAlign="right" label="保存到本地" name="save" />
|
||||
</LoginForm>
|
||||
>
|
||||
<Input
|
||||
placeholder="API baseURL"
|
||||
prefix={<GlobalOutlined className={"prefixIcon"} />}
|
||||
></Input>
|
||||
</Form.Item>
|
||||
<Form.Item name="username">
|
||||
<Input
|
||||
placeholder="username"
|
||||
prefix={<UserOutlined className={"prefixIcon"} />}
|
||||
></Input>
|
||||
</Form.Item>
|
||||
<Form.Item name="password">
|
||||
<Input.Password
|
||||
placeholder="password"
|
||||
prefix={<LockOutlined className={"prefixIcon"} />}
|
||||
></Input.Password>
|
||||
</Form.Item>
|
||||
<Form.Item name="save" valuePropName="checked">
|
||||
<Checkbox>保存到本地</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle style={{ marginBottom: "1em" }}>
|
||||
<Button block type="primary" htmlType="submit">
|
||||
链接
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<LocalServers></LocalServers>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+19
-17
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@@ -31,10 +24,9 @@ import { download, jsonFormat } from "../uitls";
|
||||
import { MonacoEditor } from "../uitls/userMonacoWorker";
|
||||
import Ctx from "../uitls/ctx";
|
||||
import * as API from "../api";
|
||||
import ListCard from "../components/ListCard";
|
||||
import ListCard, { ProCard } from "../components/ListCard";
|
||||
import ChainCard from "../components/ListCard/Chains";
|
||||
import ServiceCard from "../components/ListCard/Services";
|
||||
import { ProCard } from "@ant-design/pro-components";
|
||||
import HopsCard from "../components/ListCard/Hops";
|
||||
import { fixOldCacheConfig } from "../api/local";
|
||||
import { configEvent } from "../uitls/events";
|
||||
@@ -167,9 +159,13 @@ const Manage = () => {
|
||||
// style={{ padding: "0 15px" }}
|
||||
>
|
||||
<Col color="">
|
||||
<Button type="link" icon={<ReloadOutlined />} onClick={async () => {
|
||||
useServerConfig.set((await API.getConfig()) as any)
|
||||
}}>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={async () => {
|
||||
useServerConfig.set((await API.getConfig()) as any);
|
||||
}}
|
||||
>
|
||||
刷新配置
|
||||
</Button>
|
||||
</Col>
|
||||
@@ -229,7 +225,7 @@ const Manage = () => {
|
||||
<Form.Item name="autoSave" label="自动保存" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="saveFormat" label="格式">
|
||||
<Form.Item name="saveFormat" label="格式" initialValue={'json'}>
|
||||
<Radio.Group optionType="button" buttonStyle="solid">
|
||||
<Radio value="json">json</Radio>
|
||||
<Radio value="yaml">yaml</Radio>
|
||||
@@ -271,17 +267,23 @@ const Manage = () => {
|
||||
<Col {...colSpan}>
|
||||
<ListCard module="resolver" />
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<ListCard module="sd" />
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<ListCard module="observer" />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ProCard boxShadow title="限速限流">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col {...colSpan1}>
|
||||
<ListCard module="limiter" bordered />
|
||||
<ListCard module="limiter" bordered boxShadow={false} />
|
||||
</Col>
|
||||
<Col {...colSpan1}>
|
||||
<ListCard module="rlimiter" bordered />
|
||||
<ListCard module="rlimiter" bordered boxShadow={false} />
|
||||
</Col>
|
||||
<Col {...colSpan1}>
|
||||
<ListCard module="climiter" bordered />
|
||||
<ListCard module="climiter" bordered boxShadow={false} />
|
||||
</Col>
|
||||
</Row>
|
||||
</ProCard>
|
||||
|
||||
@@ -15,7 +15,9 @@ export const apis = {
|
||||
hops: "/config/hops",
|
||||
hosts: "/config/hosts",
|
||||
ingresses: "/config/ingresses",
|
||||
observers: "/config/observers",
|
||||
resolvers: "/config/resolvers",
|
||||
sds: "/config/sds",
|
||||
services: "/config/services",
|
||||
};
|
||||
|
||||
@@ -42,6 +44,8 @@ export const hosts = getRESTfulApi<Gost.HostsConfig>(apis["hosts"]);
|
||||
export const ingresses = getRESTfulApi<Gost.IngressConfig>(apis["ingresses"]);
|
||||
export const resolvers = getRESTfulApi<Gost.ResolverConfig>(apis["resolvers"]);
|
||||
export const services = getRESTfulApi<Gost.ServiceConfig>(apis["services"]);
|
||||
export const sds = getRESTfulApi<Gost.ServiceConfig>(apis["sds"]);
|
||||
export const observers = getRESTfulApi<Gost.ServiceConfig>(apis["observers"]);
|
||||
|
||||
// 获取当前config
|
||||
export const getConfig = (format?: Format) => require.get(apis.config);
|
||||
|
||||
@@ -95,6 +95,18 @@ const modules: Module[] = [
|
||||
// localApi: (LocalApi as any)['services'],
|
||||
// localApi: (LocalApi as any)['services'],
|
||||
},
|
||||
{
|
||||
name: "sd",
|
||||
title: "服务发现(SD)",
|
||||
subTitle: "服务发现",
|
||||
...getAttr("sds"),
|
||||
},
|
||||
{
|
||||
name: "observer",
|
||||
title: "观测器(Observer)",
|
||||
subTitle: "观测器",
|
||||
...getAttr("observers"),
|
||||
},
|
||||
];
|
||||
|
||||
export const getModule = (name: string) =>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React, { useRef } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormInstance,
|
||||
ModalFormProps,
|
||||
} from "@ant-design/pro-components";
|
||||
import { Button, Dropdown, Form, Space } from "antd";
|
||||
import { Button, Dropdown, Form, FormInstance, Space } from "antd";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { jsonFormat, jsonStringFormat, jsonParse } from "../../uitls";
|
||||
import { jsonFormat, jsonStringFormat, jsonParse, jsonEdit } from "../../uitls";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { MonacoEditor, getModel, model, modelUri } from "../../uitls/userMonacoWorker";
|
||||
import { Template } from "../../templates";
|
||||
import { render } from "react-dom";
|
||||
import { useServerConfig } from "../../uitls/server";
|
||||
// import { render } from "react-dom";
|
||||
// import { useServerConfig } from "../../uitls/server";
|
||||
import ModalForm, { ModalFormProps } from "../ModalForm";
|
||||
|
||||
const template2menu: any = (template: Template) => {
|
||||
const { children, ...other } = template;
|
||||
@@ -52,17 +48,14 @@ type JsonFromProps = ModalFormProps & {
|
||||
|
||||
const JsonForm: React.FC<JsonFromProps> = (props) => {
|
||||
const { templates, ...other } = props;
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
const formRef = useRef<FormInstance<any>>();
|
||||
const templateHandle = (json: string) => {
|
||||
let value;
|
||||
let value = json;
|
||||
if (props.initialValues?.value) {
|
||||
const _json = jsonParse(json);
|
||||
const _old = jsonParse(props.initialValues.value);
|
||||
_json.name = _old.name ? _old.name : _json.name;
|
||||
value = jsonFormat(_json);
|
||||
} else {
|
||||
value = jsonStringFormat(json);
|
||||
value = _old.name ? jsonEdit(json, [{path:'name', value: _old.name}]) : json;
|
||||
}
|
||||
value = jsonStringFormat(value);
|
||||
formRef.current?.setFieldValue("value", value);
|
||||
formRef.current?.validateFields();
|
||||
};
|
||||
@@ -236,8 +229,8 @@ export const showJsonForm = (props: JsonFromProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const showUpModelForm = (props: any) => {
|
||||
const { name, root, model } = props;
|
||||
};
|
||||
// export const showUpModelForm = (props: any) => {
|
||||
// const { name, root, model } = props;
|
||||
// };
|
||||
|
||||
export default JsonForm;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
BetaSchemaForm,
|
||||
type ProFormColumnsType,
|
||||
} from "@ant-design/pro-components";
|
||||
import { ServiceConfig } from "../../api/types";
|
||||
|
||||
const columns: ProFormColumnsType<ServiceConfig>[] = [
|
||||
{
|
||||
dataIndex:'name',
|
||||
title: "服务名",
|
||||
valueType: 'text',
|
||||
},
|
||||
{
|
||||
dataIndex:'addr',
|
||||
title: "地址端口",
|
||||
valueType: 'text',
|
||||
},
|
||||
{
|
||||
dataIndex:'addr',
|
||||
title: "地址端口",
|
||||
valueType: 'text',
|
||||
}
|
||||
];
|
||||
|
||||
const ServiceForm: React.FC<any> = (props) => {
|
||||
return <BetaSchemaForm<ServiceConfig> columns={columns}></BetaSchemaForm>;
|
||||
};
|
||||
|
||||
export default ServiceForm;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState, useMemo, useContext } from "react";
|
||||
import { getRESTfulApi } from "../../api";
|
||||
import { ProCard } from "@ant-design/pro-components";
|
||||
import { Card, CardProps } from "antd";
|
||||
import PublicList, { PublicListProps } from "../List/Public";
|
||||
import AddButton from "../Forms/AddButton";
|
||||
import { GostCommit } from "../../api/local";
|
||||
@@ -10,6 +10,7 @@ import { Button, Input, Modal, Space, notification } from "antd";
|
||||
import { getModule } from "../../api/modules";
|
||||
import { configEvent } from "../../uitls/events";
|
||||
import { CloseOutlined } from "@ant-design/icons";
|
||||
import classnames from "classnames";
|
||||
|
||||
export type ListCardProps = {
|
||||
module?: string;
|
||||
@@ -26,6 +27,14 @@ export type ListCardProps = {
|
||||
filter?: PublicListProps["filter"];
|
||||
};
|
||||
|
||||
export const ProCard = (props: CardProps & { boxShadow?: boolean }) => {
|
||||
const { boxShadow, className, ...other } = props;
|
||||
const _className = classnames(className, {
|
||||
"antd-cord-boxShadow": boxShadow,
|
||||
});
|
||||
return <Card className={_className} {...other}></Card>;
|
||||
};
|
||||
|
||||
const ListCard: React.FC<ListCardProps> = (props) => {
|
||||
const {
|
||||
title,
|
||||
@@ -42,7 +51,7 @@ const ListCard: React.FC<ListCardProps> = (props) => {
|
||||
} = useMemo(() => {
|
||||
return { ...getModule(props.module || "")!, ...props };
|
||||
}, [props]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const _prop = {
|
||||
title: subTitle || "",
|
||||
keyword,
|
||||
@@ -149,10 +158,14 @@ const ListCard: React.FC<ListCardProps> = (props) => {
|
||||
title={title}
|
||||
extra={
|
||||
<Space>
|
||||
<Input.Search allowClear onChange={(e)=>{
|
||||
const value = e.target.value;
|
||||
setKeyword(value);
|
||||
}} size="small"></Input.Search>
|
||||
<Input.Search
|
||||
allowClear
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setKeyword(value);
|
||||
}}
|
||||
size="small"
|
||||
></Input.Search>
|
||||
{/* <Button icon={""} size="small">全屏编辑</Button> */}
|
||||
<AddButton {..._prop} />
|
||||
</Space>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Col, Flex, Row, Space } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { GostApiConfig, getLocalServers, deleteLocal } from "../uitls/server";
|
||||
const LocalServers = () => {
|
||||
const [list, setList] = useState<GostApiConfig[]>();
|
||||
// const [local, setLocal] = useState<Record<string, GostApiConfig>>();
|
||||
|
||||
const updateList = useCallback(async () => {
|
||||
return getLocalServers()
|
||||
.then((local) => {
|
||||
return local.sort((a, b) => {
|
||||
const t1 = a.time || 0;
|
||||
const t2 = b.time || 0;
|
||||
return t2 - t1;
|
||||
});
|
||||
})
|
||||
.then((list) => setList(list));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
updateList();
|
||||
// getList().then(list=>setList(list))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && list?.length > 0 ? (
|
||||
<Space direction="vertical" style={{ display: "flex" }}>
|
||||
<div>快速连接</div>
|
||||
<Row gutter={10}>
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<Col
|
||||
key={item.addr}
|
||||
span={12}
|
||||
title={item.addr}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<Flex gap={5} style={{ overflow: "hidden" }}>
|
||||
<a
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
flex: "auto",
|
||||
}}
|
||||
href={`?use=${item.addr}`}
|
||||
>
|
||||
{item.addr}
|
||||
</a>
|
||||
<DeleteOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={async () => {
|
||||
await deleteLocal(item.addr);
|
||||
updateList();
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Space>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalServers;
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import { Form, Modal } from "antd";
|
||||
import type { FormInstance, FormProps, ModalProps } from "antd";
|
||||
import { useBindValue } from "../uitls/use";
|
||||
|
||||
export type ModalFormProps<T = Record<string, any>> = Omit<
|
||||
FormProps<T>,
|
||||
"onFinish" | "title"
|
||||
> & {
|
||||
/**
|
||||
* 接收任意值,返回 真值 会关掉这个抽屉
|
||||
*
|
||||
* @name 表单结束后调用
|
||||
*
|
||||
* @example 结束后关闭抽屉
|
||||
* onFinish: async ()=> {await save(); return true}
|
||||
*
|
||||
* @example 结束后不关闭抽屉
|
||||
* onFinish: async ()=> {await save(); return false}
|
||||
*/
|
||||
onFinish?: (formData: T) => Promise<any>;
|
||||
// /** @name 提交数据时,禁用取消按钮的超时时间(毫秒)。 */
|
||||
// submitTimeout?: number;
|
||||
/** @name 用于触发抽屉打开的 dom */
|
||||
trigger?: JSX.Element;
|
||||
/** @name 受控的打开关闭 */
|
||||
open?: ModalProps["open"];
|
||||
/** @name 打开关闭的事件 */
|
||||
onOpenChange?: (visible: boolean) => void;
|
||||
/**
|
||||
* 不支持 'visible',请使用全局的 visible
|
||||
*
|
||||
* @name 弹框的属性
|
||||
*/
|
||||
modalProps?: Omit<ModalProps, "visible">;
|
||||
/** @name 弹框的标题 */
|
||||
title?: ModalProps["title"];
|
||||
/** @name 弹框的宽度 */
|
||||
width?: ModalProps["width"];
|
||||
formRef?: any;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const ModalForm: React.FC<ModalFormProps> = (props) => {
|
||||
const {
|
||||
onFinish: propOnFinish,
|
||||
trigger,
|
||||
open: propOpen,
|
||||
onOpenChange: propOnOpenChange,
|
||||
modalProps,
|
||||
title,
|
||||
width,
|
||||
formRef,
|
||||
children,
|
||||
...other
|
||||
} = props;
|
||||
const [open, setOpen] = useBindValue(propOpen, false);
|
||||
const [form] = Form.useForm();
|
||||
const onCancel = useCallback(
|
||||
() => {
|
||||
setOpen(false);
|
||||
propOnOpenChange?.(false);
|
||||
modalProps?.onCancel?.({} as any)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[propOnOpenChange]
|
||||
);
|
||||
const afterClose = useCallback(() => {
|
||||
form.resetFields();
|
||||
modalProps?.afterClose?.()
|
||||
},[form, modalProps])
|
||||
const onFinish = useMemo(() => {
|
||||
return async function (value: any) {
|
||||
const v = await propOnFinish?.apply(null, [value]);
|
||||
if (v === true) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
}, [propOnFinish, onCancel]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const _modalProps: ModalProps = useMemo(() => {
|
||||
return {
|
||||
...modalProps,
|
||||
onCancel,
|
||||
onOk: () => {
|
||||
form.submit();
|
||||
},
|
||||
afterClose,
|
||||
open,
|
||||
title,
|
||||
width,
|
||||
};
|
||||
}, [afterClose, form, modalProps, onCancel, open, title, width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formRef) return;
|
||||
const type = typeof formRef;
|
||||
switch (type) {
|
||||
case "function":
|
||||
formRef(form);
|
||||
break;
|
||||
case "object":
|
||||
formRef.current = form;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [form, formRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal {..._modalProps}>
|
||||
<Form form={form} onFinish={onFinish} {...other}>
|
||||
{children}
|
||||
</Form>
|
||||
</Modal>
|
||||
{trigger &&
|
||||
React.cloneElement(trigger, {
|
||||
onClick: () => {
|
||||
setOpen(true);
|
||||
},
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalForm;
|
||||
+4
-9
@@ -1,10 +1,5 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
||||
<App />
|
||||
,
|
||||
)
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
@@ -12,7 +12,7 @@ const defaultTemplates = `{
|
||||
// "resolver": "resolver-0",
|
||||
// "hosts": "hosts-0",
|
||||
"handler": {
|
||||
"type": "http",
|
||||
"type": "auto",
|
||||
// "auth": {
|
||||
// "username": "gost",
|
||||
// "password": "gost"
|
||||
@@ -32,7 +32,7 @@ const defaultTemplates = `{
|
||||
// "password": "pass"
|
||||
// },
|
||||
// "auther": "auther-0",
|
||||
"chain": "chain-0",
|
||||
// "chain": "chain-0",
|
||||
// "tls": {
|
||||
// "certFile": "cert.pem",
|
||||
// "keyFile": "key.pem",
|
||||
|
||||
@@ -12,6 +12,8 @@ import climiters from "./climiters";
|
||||
import limiters from "./limiters";
|
||||
import rlimiters from "./rlimiters";
|
||||
import resolvers from "./resolvers";
|
||||
import sds from "./sds";
|
||||
import observers from "./observers";
|
||||
import { Template } from "./type";
|
||||
|
||||
export type { Template };
|
||||
@@ -33,6 +35,8 @@ export default {
|
||||
ingresses: ingresses,
|
||||
resolvers: resolvers,
|
||||
routers: routers,
|
||||
sds: sds,
|
||||
observers: observers,
|
||||
|
||||
services: services,
|
||||
} as Record<string, Template[]>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getByName } from "./default";
|
||||
import { getOtherAll } from "./otherOrigin";
|
||||
import { Template } from "./type";
|
||||
|
||||
const def = getByName("observers");
|
||||
export default [
|
||||
def
|
||||
? def
|
||||
: {
|
||||
label: "插件",
|
||||
json: `
|
||||
// https://gost.run/concepts/observer/
|
||||
{
|
||||
"name": "observer-0",
|
||||
"plugin": {
|
||||
"type": "grpc",
|
||||
// "type": "http",
|
||||
"addr": "127.0.0.1:8000",
|
||||
"token": "gost",
|
||||
// "tls": {}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
] as Template[];
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getByName } from "./default";
|
||||
import { getOtherAll } from "./otherOrigin";
|
||||
import { Template } from "./type";
|
||||
|
||||
const def = getByName("sds");
|
||||
export default [
|
||||
def
|
||||
? def
|
||||
: {
|
||||
label: "HTTP插件",
|
||||
json: `
|
||||
// https://gost.run/concepts/sd/
|
||||
{
|
||||
"name": "sd-0",
|
||||
"plugin": {
|
||||
"type": "http",
|
||||
"addr": "http://127.0.0.1:8000/sd",
|
||||
// "token": "gost",
|
||||
// "tls": {}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
] as Template[];
|
||||
+26
-3
@@ -1,13 +1,26 @@
|
||||
import { format, parse, applyEdits } from "jsonc-parser";
|
||||
import {
|
||||
format,
|
||||
parse,
|
||||
applyEdits,
|
||||
ModificationOptions,
|
||||
JSONPath,
|
||||
modify,
|
||||
EditResult,
|
||||
} from "jsonc-parser";
|
||||
|
||||
const localCacheKeys = ['_id_', '_key_', '_type_'];
|
||||
const localCacheKeys = ["_id_", "_key_", "_type_"];
|
||||
type modify = {
|
||||
path: JSONPath | string;
|
||||
value: any;
|
||||
options?: ModificationOptions;
|
||||
};
|
||||
|
||||
export const jsonFormat = (json: any) => {
|
||||
return JSON.stringify(json, null, 4);
|
||||
};
|
||||
|
||||
export const jsonFormatValue = (json: any) => {
|
||||
const newJson = {...json};
|
||||
const newJson = { ...json };
|
||||
for (const key of localCacheKeys) {
|
||||
delete newJson[key];
|
||||
}
|
||||
@@ -31,6 +44,16 @@ export const jsonParse = (str: string) => {
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const jsonEdit = (str: string, edits: modify[]) => {
|
||||
const results:EditResult = [];
|
||||
const run = modify.bind(null,str);
|
||||
edits.forEach(({path,value,options},index)=>{
|
||||
path = Array.isArray(path) ? path : path.split('.');
|
||||
results.push(...run(path,value,options || {}))
|
||||
})
|
||||
return applyEdits(str, results);
|
||||
};
|
||||
|
||||
export const download = (content: BlobPart, filename: string) => {
|
||||
const href = URL.createObjectURL(new Blob([content]));
|
||||
const link = document.createElement("a");
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
/**
|
||||
* 一般用于input组件, 入参 value(受控) , defaultValue(不受控) 与组件内部 使用 value 的协调
|
||||
* @param value (受控)
|
||||
* @param defaultValue (不受控)
|
||||
*
|
||||
* 一般还需要结合 props.onChange 方法, 同步 value(受控)
|
||||
* const [v,setV] = useBindValue<string>(props.value, props.defaultValue)
|
||||
* const asyncValue = (value:string)=>{
|
||||
* setV(value);
|
||||
* props.onChange(value)
|
||||
* }
|
||||
*/
|
||||
function useBindValue<T>(
|
||||
value: T | undefined,
|
||||
defaultValue: T | undefined
|
||||
): [T | undefined, React.Dispatch<React.SetStateAction<T>>];
|
||||
/**
|
||||
*
|
||||
* @param value (受控)
|
||||
* @param defaultValue (不受控)
|
||||
* @param defaultV 默认值
|
||||
*/
|
||||
function useBindValue<T>(
|
||||
value: T | undefined,
|
||||
defaultValue: T | undefined,
|
||||
defaultV: T
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>];
|
||||
/**
|
||||
*
|
||||
* @param value (受控)
|
||||
* @param defaultValue (不受控)
|
||||
* @param defaultV 默认值
|
||||
* @returns 反回入理过的 value 与 setValue
|
||||
*/
|
||||
function useBindValue<T>(
|
||||
value: T | undefined,
|
||||
defaultValue: T | undefined,
|
||||
defaultV?: T
|
||||
): [T | undefined, React.Dispatch<React.SetStateAction<T>>] {
|
||||
const inputValue = value !== undefined;
|
||||
const inputDefaultValue = defaultValue !== undefined;
|
||||
const [_value, _setValue] = useState<T | undefined>(inputValue ? value : inputDefaultValue ? defaultValue : defaultV);
|
||||
const that = useRef({
|
||||
value: _value,
|
||||
setValue: (s: T | ((v: T) => T)) => {
|
||||
let nextValue: T;
|
||||
if (typeof s === 'function') {
|
||||
nextValue = (s as (v: T) => T)(that.current.value as T);
|
||||
} else {
|
||||
nextValue = s;
|
||||
}
|
||||
that.current.value = nextValue;
|
||||
_setValue(nextValue);
|
||||
},
|
||||
});
|
||||
const status = useMemo<T | undefined>(() => {
|
||||
const inputValue = value !== undefined;
|
||||
const status = inputValue ? value : _value;
|
||||
that.current.value = status;
|
||||
return status;
|
||||
}, [value, _value]);
|
||||
return [status, that.current.setValue];
|
||||
}
|
||||
|
||||
export { useBindValue };
|
||||
+5
-5
@@ -6,7 +6,9 @@ import { analyzer } from "vite-bundle-analyzer";
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const config: UserConfig = {
|
||||
base: "./",
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react()
|
||||
],
|
||||
};
|
||||
|
||||
if (mode === "development") {
|
||||
@@ -21,8 +23,6 @@ export default defineConfig(({ command, mode }) => {
|
||||
manualChunks: {
|
||||
monaco: ["monaco-editor"],
|
||||
antd: ["antd"],
|
||||
// 将组件库的代码打包
|
||||
"ant-design": ["@ant-design/icons", "@ant-design/pro-components"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -30,8 +30,8 @@ export default defineConfig(({ command, mode }) => {
|
||||
}
|
||||
|
||||
if (mode === "analyzer") {
|
||||
config.plugins.unshift(analyzer() as any);
|
||||
config.build.sourcemap="hidden";
|
||||
config.plugins.push(analyzer());
|
||||
config.build.sourcemap = "hidden";
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
Reference in New Issue
Block a user