mirror of
https://github.com/MetaCubeX/metacubexd.git
synced 2024-12-24 06:44:11 +08:00
chore: initial commit
This commit is contained in:
commit
b8c6c2fa7a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
||||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="shortcut icon" type="image/svg" href="/src/assets/favicon.svg" />
|
||||
<title>Solid App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "vite-template-solid",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"serve": "vite preview",
|
||||
"start": "vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@felte/solid": "^1.2.10",
|
||||
"@felte/validator-zod": "^1.0.16",
|
||||
"@solid-primitives/event-listener": "^2.2.14",
|
||||
"@solid-primitives/storage": "^2.1.1",
|
||||
"@solid-primitives/websocket": "^1.1.0",
|
||||
"@solidjs/router": "^0.8.3",
|
||||
"@tabler/icons-solidjs": "^2.32.0",
|
||||
"@tanstack/solid-table": "^8.9.3",
|
||||
"@tanstack/solid-virtual": "3.0.0-beta.6",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/node": "^20.5.6",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"apexcharts": "^3.42.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"byte-size": "^8.1.1",
|
||||
"daisyui": "^3.6.3",
|
||||
"is-ip": "^5.0.1",
|
||||
"ky": "^0.33.3",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.3",
|
||||
"solid-apexcharts": "^0.3.2",
|
||||
"solid-devtools": "^0.27.7",
|
||||
"solid-form-handler": "^1.2.0",
|
||||
"solid-js": "^1.7.11",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
2131
pnpm-lock.yaml
generated
Normal file
2131
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
71
src/App.tsx
Normal file
71
src/App.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Route, Routes, useLocation, useNavigate } from '@solidjs/router'
|
||||
import { For, Show, onMount } from 'solid-js'
|
||||
import { Header } from '~/components/Header'
|
||||
import { themes } from '~/constants'
|
||||
import { Config } from '~/pages/Config'
|
||||
import { Connections } from '~/pages/Connections'
|
||||
import { Logs } from '~/pages/Logs'
|
||||
import { Overview } from '~/pages/Overview'
|
||||
import { Proxies } from '~/pages/Proxies'
|
||||
import { Rules } from '~/pages/Rules'
|
||||
import { Setup } from '~/pages/Setup'
|
||||
import { curTheme, selectedEndpoint, setCurTheme } from '~/signals'
|
||||
|
||||
export const App = () => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
onMount(() => {
|
||||
if (!selectedEndpoint()) {
|
||||
navigate('/setup')
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="app relative" data-theme={curTheme()}>
|
||||
<div class="sticky inset-x-0 top-0 z-50 flex items-center rounded-md bg-base-200 px-4 py-2">
|
||||
<Show when={location.pathname !== '/setup'}>
|
||||
<Header />
|
||||
</Show>
|
||||
|
||||
<div class="dropdown-end dropdown-hover dropdown ml-auto">
|
||||
<label tabindex="0" class="btn btn-sm m-2 uppercase">
|
||||
Themes
|
||||
</label>
|
||||
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box z-[1] gap-2 bg-base-300 p-2 shadow"
|
||||
>
|
||||
<For each={themes}>
|
||||
{(theme) => (
|
||||
<li
|
||||
data-theme={theme}
|
||||
class="btn btn-xs"
|
||||
onClick={() => setCurTheme(theme)}
|
||||
>
|
||||
{theme}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
<Routes>
|
||||
<Show when={selectedEndpoint()}>
|
||||
<Route path="/" component={Overview} />
|
||||
<Route path="/proxies" component={Proxies} />
|
||||
<Route path="/rules" component={Rules} />
|
||||
<Route path="/conns" component={Connections} />
|
||||
<Route path="/logs" component={Logs} />
|
||||
<Route path="/config" component={Config} />
|
||||
</Show>
|
||||
|
||||
<Route path="/setup" component={Setup} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
51
src/assets/favicon.svg
Normal file
51
src/assets/favicon.svg
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 256 128" style="enable-background:new 0 0 256 128;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#333333;fill-opacity:0;}
|
||||
.st1{opacity:0.8;fill:#E62C5A;enable-background:new;}
|
||||
.st2{opacity:0.8;fill:#0FABF6;enable-background:new;}
|
||||
.st3{opacity:0.5;fill:#501B8D;enable-background:new;}
|
||||
.st4{fill:#333333;}
|
||||
</style>
|
||||
<g transform="translate(81.56239318847656,79.98116683959961)">
|
||||
<g transform="translate(58.43760681152344,0)">
|
||||
<g>
|
||||
<rect x="-51.3" y="-72.1" class="st0" width="78.6" height="85.2"/>
|
||||
<path class="st1" d="M14-24.9l-30.9-30.7l-15.6-15.5c-1.6-0.7-3.2-1-4.9-1c-7.5,0-13.6,6.9-13.8,15.5v37
|
||||
c-0.1,8.8,6.9,16,15.7,16.1c0,0,0,0,0,0H7.7c5.2-2.5,8.5-7.8,8.5-13.6C16.2-19.8,15.4-22.6,14-24.9z"/>
|
||||
<path class="st2" d="M11.6-55.6h-43.3c-5.2,2.5-8.5,7.8-8.5,13.6c0,2.8,0.8,5.5,2.2,7.8L-7.1-3.5L8.6,12.1c1.6,0.7,3.2,1,4.9,1
|
||||
c7.5,0,13.6-6.9,13.8-15.5v-37.1C27.4-48.2,20.4-55.4,11.6-55.6z"/>
|
||||
<path class="st3" d="M-38-34.1L-7.1-3.5H7.7c5.2-2.5,8.5-7.8,8.5-13.6c0-2.8-0.8-5.5-2.2-7.8l-30.9-30.7h-14.8
|
||||
c-5.2,2.5-8.5,7.8-8.5,13.6C-40.2-39.2-39.4-36.5-38-34.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0,72.08086395263672)">
|
||||
<g>
|
||||
<g transform="scale(0.35999999999999927)">
|
||||
<g>
|
||||
<path class="st4" d="M-23.8-110l-12.6-19.6c-1-1.9-3.5-1.3-3.5,0.9V-99c0,2.5,3.8,2.5,3.8,0v-23.4l10.5,16.4
|
||||
c0.8,1.5,2.7,1.5,3.6,0l10.5-16.5V-99c0,2.5,3.8,2.5,3.8,0v-29.7c0-2.2-2.4-2.8-3.6-0.9L-23.8-110z M5.1-112.2h19.4
|
||||
c2.2,0,2.1-3.3,0-3.3H5.1v-11.6h20.4c2.2,0,2.3-3.3,0-3.3H3.7c-1.4,0-2.4,1-2.4,2.2v28.3c0,1.2,0.9,2.3,2.4,2.3h21.7
|
||||
c2.5,0,2.2-3.4,0-3.4H5.1V-112.2z M32.7-130.4c-2.4,0-2.4,3.6,0,3.6h11.4v28c0,1.3,1,1.9,1.9,1.9c0.9,0,2-0.6,2-1.9
|
||||
c0-9.3-0.1-18.7-0.1-28h11.5c2.5,0,2.5-3.6,0-3.6H32.7z M82.5-109.5H66.3l8.1-16.2L82.5-109.5z M84.1-106.2l4,8
|
||||
c1,2,4.8,0.7,3.5-1.5l-15.5-30.5c-0.4-0.7-1-1-1.7-1c-0.7,0-1.4,0.3-1.7,1L57.1-99.6c-0.9,2.1,2.5,3.4,3.4,1.5l4.2-8.1H84.1z
|
||||
M124-103.5c1.3-1.9-1.8-3.8-3-2.1c-1.1,1.4-2.6,2.6-4.2,3.5c-2,1-4.1,1.5-6,1.5c-7.1,0-13.1-6-13.1-13.8
|
||||
c0-7.5,4.5-12,10.8-12.9c4.5-0.5,8.6,0,12.1,3.8c1.4,1.5,4.2-0.6,2.7-2.3c-4.1-4.3-9.3-5.8-15.4-5c-7.9,1.2-14,7.9-14,16.4
|
||||
c0,10.2,7.9,17.4,16.8,17.4c2.5,0,5.3-0.7,7.7-1.9C120.6-99.9,122.6-101.5,124-103.5z M156.7-112.1c0,14.9-22.7,14.9-22.7,0
|
||||
V-129c0-2.3-3.9-2.3-3.9,0v17c0,9.9,7.7,14.9,15.2,14.9c7.5,0,15.2-5,15.2-14.9v-17c0-2.5-3.9-2.5-3.9,0V-112.1z M198.1-107.3
|
||||
c0-4.2-2.6-6.8-6.3-8c2.7-1.1,4-3.5,4-6.1c0-7.3-7.4-8.9-12.2-8.9h-12.2c-1,0-1.9,0.8-1.9,1.9v29.1c0,1,0.9,1.9,1.9,1.9h13.4
|
||||
C191.2-97.6,198.1-99.6,198.1-107.3z M183.5-127c3.2,0,8.5,1.1,8.5,5.6c0,3.8-3.9,5-8.3,5h-10.5V-127H183.5z M173.2-113.1h13.1
|
||||
c4.5,0,8,1.8,8,5.8c0,5.2-5,6.4-9.5,6.4h-11.6V-113.1z M208.5-112.2h19.4c2.2,0,2.1-3.3,0-3.3h-19.4v-11.6h20.4
|
||||
c2.2,0,2.3-3.3,0-3.3h-21.8c-1.4,0-2.4,1-2.4,2.2v28.3c0,1.2,0.9,2.3,2.4,2.3h21.7c2.5,0,2.2-3.4,0-3.4h-20.4V-112.2z
|
||||
M238.4-130.3c-1.5-1.8-4.3,0.5-2.8,2.2l11.9,13.7l-12.3,14c-1.7,2,0.9,4.4,2.8,2.3l11.9-13.7L261.7-98c2,2,4.6-0.1,2.9-2.3
|
||||
l-12.3-14.1l12-13.6c1.6-1.8-1.3-4.1-2.8-2.3l-11.4,13.4L238.4-130.3z M272.6-127.1h9.9c7.7,0,11.6,6.6,11.6,13.1
|
||||
c0,6.5-3.9,13-11.6,13h-9.9V-127.1z M282.5-97.6c10.3,0,15.4-8.2,15.4-16.4c0-8.2-5.1-16.4-15.4-16.4h-11.2
|
||||
c-1.3,0-2.4,1-2.4,2.2v28.4c0,1.2,1.1,2.2,2.4,2.2H282.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
69
src/components/Header.tsx
Normal file
69
src/components/Header.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { A, useNavigate } from '@solidjs/router'
|
||||
import {
|
||||
IconFileStack,
|
||||
IconGlobe,
|
||||
IconHome,
|
||||
IconNetwork,
|
||||
IconNetworkOff,
|
||||
IconRuler,
|
||||
IconSettings,
|
||||
} from '@tabler/icons-solidjs'
|
||||
import { setSelectedEndpoint } from '~/signals'
|
||||
|
||||
export const Header = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<ul class="menu rounded-box menu-horizontal">
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/" data-tip="Home">
|
||||
<IconHome />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/proxies" data-tip="Proxies">
|
||||
<IconGlobe />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/rules" data-tip="Rules">
|
||||
<IconRuler />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/conns" data-tip="Connections">
|
||||
<IconNetwork />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/logs" data-tip="Logs">
|
||||
<IconFileStack />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<A class="tooltip tooltip-bottom" href="/config" data-tip="Config">
|
||||
<IconSettings />
|
||||
</A>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
class="tooltip tooltip-bottom"
|
||||
data-tip="Switch Endpoint"
|
||||
onClick={() => {
|
||||
setSelectedEndpoint('')
|
||||
|
||||
navigate('/setup')
|
||||
}}
|
||||
>
|
||||
<IconNetworkOff />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
31
src/constants.ts
Normal file
31
src/constants.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export const themes = [
|
||||
'light',
|
||||
'dark',
|
||||
'cupcake',
|
||||
'bumblebee',
|
||||
'emerald',
|
||||
'corporate',
|
||||
'synthwave',
|
||||
'retro',
|
||||
'cyberpunk',
|
||||
'valentine',
|
||||
'halloween',
|
||||
'garden',
|
||||
'forest',
|
||||
'aqua',
|
||||
'lofi',
|
||||
'pastel',
|
||||
'fantasy',
|
||||
'wireframe',
|
||||
'black',
|
||||
'luxury',
|
||||
'dracula',
|
||||
'cmyk',
|
||||
'autumn',
|
||||
'business',
|
||||
'acid',
|
||||
'lemonade',
|
||||
'night',
|
||||
'coffee',
|
||||
'winter',
|
||||
]
|
10
src/index.css
Normal file
10
src/index.css
Normal file
@ -0,0 +1,10 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
@apply subpixel-antialiased;
|
||||
}
|
18
src/index.tsx
Normal file
18
src/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
/* @refresh reload */
|
||||
import 'solid-devtools'
|
||||
import { render } from 'solid-js/web'
|
||||
|
||||
import { Router } from '@solidjs/router'
|
||||
import { App } from './App'
|
||||
import './index.css'
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
render(
|
||||
() => (
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
),
|
||||
root!,
|
||||
)
|
3
src/pages/Config.tsx
Normal file
3
src/pages/Config.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Config = () => {
|
||||
return <div>config</div>
|
||||
}
|
162
src/pages/Connections.tsx
Normal file
162
src/pages/Connections.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { createEventSignal } from '@solid-primitives/event-listener'
|
||||
import { createReconnectingWS } from '@solid-primitives/websocket'
|
||||
import {
|
||||
ColumnDef,
|
||||
createSolidTable,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
} from '@tanstack/solid-table'
|
||||
import byteSize from 'byte-size'
|
||||
import { isIPv6 } from 'is-ip'
|
||||
import { For, createSignal } from 'solid-js'
|
||||
import { wsEndpointURL } from '~/signals'
|
||||
import type { Connection } from '../types'
|
||||
|
||||
export const Connections = () => {
|
||||
const [search, setSearch] = createSignal('')
|
||||
|
||||
const ws = createReconnectingWS(`${wsEndpointURL()}/connections`)
|
||||
|
||||
const messageEvent = createEventSignal<{
|
||||
message: WebSocketEventMap['message']
|
||||
}>(ws, 'message')
|
||||
|
||||
const connections = () => {
|
||||
const data = messageEvent()?.data
|
||||
|
||||
if (!data) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (
|
||||
JSON.parse(data) as { connections: Connection[] }
|
||||
).connections.slice(-100)
|
||||
}
|
||||
|
||||
const columns: ColumnDef<Connection>[] = [
|
||||
{
|
||||
accessorKey: 'ID',
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Network',
|
||||
accessorFn: (row) => row.metadata.network,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Download',
|
||||
accessorFn: (row) => byteSize(row.download),
|
||||
},
|
||||
{
|
||||
accessorKey: 'Upload',
|
||||
accessorFn: (row) => byteSize(row.upload),
|
||||
},
|
||||
{
|
||||
accessorKey: 'Rule',
|
||||
accessorFn: (row) => row.rule,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Chains',
|
||||
accessorFn: (row) => row.chains.join(' -> '),
|
||||
},
|
||||
{
|
||||
accessorKey: 'Remote Destination',
|
||||
accessorFn: (row) => row.metadata.remoteDestination,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Host',
|
||||
accessorFn: (row) => row.metadata.host,
|
||||
},
|
||||
{
|
||||
accessorKey: 'DNS Mode',
|
||||
accessorFn: (row) => row.metadata.dnsMode,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Type',
|
||||
accessorFn: (row) => row.metadata.type,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Source',
|
||||
accessorFn: (row) =>
|
||||
isIPv6(row.metadata.sourceIP)
|
||||
? `[${row.metadata.sourceIP}]:${row.metadata.sourcePort}`
|
||||
: `${row.metadata.sourceIP}:${row.metadata.sourcePort}`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'Destination',
|
||||
accessorFn: (row) =>
|
||||
isIPv6(row.metadata.destinationIP)
|
||||
? `[${row.metadata.destinationIP}]:${row.metadata.destinationPort}`
|
||||
: `${row.metadata.destinationIP}:${row.metadata.destinationPort}`,
|
||||
},
|
||||
]
|
||||
|
||||
const table = createSolidTable({
|
||||
get data() {
|
||||
return search()
|
||||
? connections().filter((connection) =>
|
||||
Object.values(connection).some((conn) =>
|
||||
JSON.stringify(conn)
|
||||
.toLowerCase()
|
||||
.includes(search().toLowerCase()),
|
||||
),
|
||||
)
|
||||
: connections()
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-4">
|
||||
<input
|
||||
class="input input-primary"
|
||||
placeholder="Search"
|
||||
onInput={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div class="overflow-x-auto whitespace-nowrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<For each={table.getHeaderGroups()}>
|
||||
{(headerGroup) => (
|
||||
<tr>
|
||||
<For each={headerGroup.headers}>
|
||||
{(header) => (
|
||||
<th>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<For each={table.getRowModel().rows}>
|
||||
{(row) => (
|
||||
<tr>
|
||||
<For each={row.getVisibleCells()}>
|
||||
{(cell) => (
|
||||
<td>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
58
src/pages/Logs.tsx
Normal file
58
src/pages/Logs.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { createEventSignal } from '@solid-primitives/event-listener'
|
||||
import { createReconnectingWS } from '@solid-primitives/websocket'
|
||||
import { For, createEffect, createSignal } from 'solid-js'
|
||||
import { wsEndpointURL } from '~/signals'
|
||||
|
||||
export const Logs = () => {
|
||||
const [search, setSearch] = createSignal('')
|
||||
const [logs, setLogs] = createSignal<string[]>([])
|
||||
|
||||
const ws = createReconnectingWS(`${wsEndpointURL()}/logs`)
|
||||
|
||||
const messageEvent = createEventSignal<{
|
||||
message: WebSocketEventMap['message']
|
||||
}>(ws, 'message')
|
||||
|
||||
createEffect(() => {
|
||||
const data = messageEvent()?.data
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
setLogs((logs) =>
|
||||
[
|
||||
...logs,
|
||||
(JSON.parse(data) as { type: string; payload: string }).payload,
|
||||
].slice(-100),
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-4">
|
||||
<input
|
||||
class="input input-primary"
|
||||
placeholder="Search"
|
||||
onInput={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div class="overflow-x-auto whitespace-nowrap">
|
||||
<For
|
||||
each={
|
||||
search()
|
||||
? logs().filter((log) =>
|
||||
log.toLowerCase().includes(search().toLowerCase()),
|
||||
)
|
||||
: logs()
|
||||
}
|
||||
>
|
||||
{(log) => (
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">{log}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
181
src/pages/Overview.tsx
Normal file
181
src/pages/Overview.tsx
Normal file
@ -0,0 +1,181 @@
|
||||
import { createEventSignal } from '@solid-primitives/event-listener'
|
||||
import { createReconnectingWS } from '@solid-primitives/websocket'
|
||||
import { ApexOptions } from 'apexcharts'
|
||||
import byteSize from 'byte-size'
|
||||
import { SolidApexCharts } from 'solid-apexcharts'
|
||||
import { createEffect, createMemo, createSignal } from 'solid-js'
|
||||
import { wsEndpointURL } from '~/signals'
|
||||
import type { Connection } from '~/types'
|
||||
|
||||
const defaultChartOptions: ApexOptions = {
|
||||
chart: {
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
animations: { easing: 'linear', dynamicAnimation: { speed: 1000 } },
|
||||
},
|
||||
noData: { text: 'Loading...' },
|
||||
legend: {
|
||||
fontSize: '14px',
|
||||
labels: { colors: 'gray' },
|
||||
itemMargin: { horizontal: 64 },
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
grid: { yaxis: { lines: { show: false } } },
|
||||
stroke: { curve: 'smooth' },
|
||||
tooltip: { enabled: false },
|
||||
xaxis: { labels: { show: false }, axisTicks: { show: false } },
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: 'gray' },
|
||||
formatter(val) {
|
||||
return byteSize(val).toString()
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Overview = () => {
|
||||
const [traffics, setTraffics] = createSignal<{ down: number; up: number }[]>(
|
||||
[],
|
||||
)
|
||||
const [memories, setMemories] = createSignal<number[]>([])
|
||||
|
||||
const trafficWS = createReconnectingWS(`${wsEndpointURL()}/traffic`)
|
||||
|
||||
const trafficWSMessageEvent = createEventSignal<{
|
||||
message: WebSocketEventMap['message']
|
||||
}>(trafficWS, 'message')
|
||||
|
||||
const traffic = () => {
|
||||
const data = trafficWSMessageEvent()?.data
|
||||
|
||||
return data ? (JSON.parse(data) as { down: number; up: number }) : null
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const t = traffic()
|
||||
|
||||
if (t) {
|
||||
setTraffics((traffics) => [...traffics, t].slice(-100))
|
||||
}
|
||||
})
|
||||
|
||||
const trafficChartOptions = createMemo<ApexOptions>(() => ({
|
||||
title: { text: 'Traffic', align: 'center', style: { color: 'gray' } },
|
||||
...defaultChartOptions,
|
||||
}))
|
||||
|
||||
const trafficChartSeries = createMemo(() => [
|
||||
{
|
||||
name: 'Down',
|
||||
data: traffics().map((t) => t.down),
|
||||
},
|
||||
{
|
||||
name: 'Up',
|
||||
data: traffics().map((t) => t.up),
|
||||
},
|
||||
])
|
||||
|
||||
const memoryWS = createReconnectingWS(`${wsEndpointURL()}/memory`)
|
||||
|
||||
const memoryWSMessageEvent = createEventSignal<{
|
||||
message: WebSocketEventMap['message']
|
||||
}>(memoryWS, 'message')
|
||||
|
||||
const memory = () => {
|
||||
const data = memoryWSMessageEvent()?.data
|
||||
|
||||
return data ? (JSON.parse(data) as { inuse: number }).inuse : null
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const m = memory()
|
||||
|
||||
if (m) {
|
||||
setMemories((memories) => [...memories, m].slice(-100))
|
||||
}
|
||||
})
|
||||
|
||||
const memoryChartOptions = createMemo<ApexOptions>(() => ({
|
||||
title: { text: 'Memory', align: 'center', style: { color: 'gray' } },
|
||||
...defaultChartOptions,
|
||||
}))
|
||||
|
||||
const memoryChartSeries = createMemo(() => [
|
||||
{
|
||||
name: 'memory',
|
||||
data: memories(),
|
||||
},
|
||||
])
|
||||
|
||||
const connectionsWS = createReconnectingWS(`${wsEndpointURL()}/connections`)
|
||||
|
||||
const connectionsWSMessageEvent = createEventSignal<{
|
||||
message: WebSocketEventMap['message']
|
||||
}>(connectionsWS, 'message')
|
||||
|
||||
const connection = () => {
|
||||
const data = connectionsWSMessageEvent()?.data
|
||||
|
||||
return data
|
||||
? (JSON.parse(data) as {
|
||||
downloadTotal: number
|
||||
uploadTotal: number
|
||||
connections: Connection[]
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="stats w-full shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Upload</div>
|
||||
<div class="stat-value">
|
||||
{byteSize(traffic()?.up || 0).toString()}/s
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Download</div>
|
||||
<div class="stat-value">
|
||||
{byteSize(traffic()?.down || 0).toString()}/s
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Upload Total</div>
|
||||
<div class="stat-value">
|
||||
{byteSize(connection()?.uploadTotal || 0).toString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Download Total</div>
|
||||
<div class="stat-value">
|
||||
{byteSize(connection()?.downloadTotal || 0).toString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Active Connections</div>
|
||||
<div class="stat-value">{connection()?.connections.length || 0}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Memory Usage</div>
|
||||
<div class="stat-value">{byteSize(memory() || 0).toString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto grid w-full max-w-screen-lg grid-cols-1 gap-4">
|
||||
<SolidApexCharts
|
||||
type="area"
|
||||
options={trafficChartOptions()}
|
||||
series={trafficChartSeries()}
|
||||
/>
|
||||
|
||||
<SolidApexCharts
|
||||
type="line"
|
||||
options={memoryChartOptions()}
|
||||
series={memoryChartSeries()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
55
src/pages/Proxies.tsx
Normal file
55
src/pages/Proxies.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { For, createSignal, onMount } from 'solid-js'
|
||||
import { useRequest } from '~/signals'
|
||||
import { Proxy, ProxyProvider } from '~/types'
|
||||
|
||||
export const Proxies = () => {
|
||||
const request = useRequest()
|
||||
const [proxies, setProxies] = createSignal<Proxy[]>([])
|
||||
const [proxyProviders, setProxyProviders] = createSignal<ProxyProvider[]>([])
|
||||
|
||||
onMount(async () => {
|
||||
const { proxies } = await request
|
||||
.get('proxies')
|
||||
.json<{ proxies: Record<string, Proxy> }>()
|
||||
|
||||
setProxies(Object.values(proxies))
|
||||
|
||||
const { providers } = await request
|
||||
.get('providers/proxies')
|
||||
.json<{ providers: Record<string, ProxyProvider> }>()
|
||||
|
||||
setProxyProviders(Object.values(providers))
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="py-4 text-lg font-semibold">Proxies</h1>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<For each={proxies()}>
|
||||
{(proxy) => (
|
||||
<div class="card card-bordered card-compact border-secondary p-4">
|
||||
{proxy.name}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="py-4 text-lg font-semibold">Proxy Providers</h1>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<For each={proxyProviders()}>
|
||||
{(proxy) => (
|
||||
<div class="card card-bordered card-compact border-secondary p-4">
|
||||
{proxy.name}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
3
src/pages/Rules.tsx
Normal file
3
src/pages/Rules.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Rules = () => {
|
||||
return <div>rules</div>
|
||||
}
|
99
src/pages/Setup.tsx
Normal file
99
src/pages/Setup.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { createForm } from '@felte/solid'
|
||||
import { validator } from '@felte/validator-zod'
|
||||
import { useNavigate } from '@solidjs/router'
|
||||
import { IconX } from '@tabler/icons-solidjs'
|
||||
import ky from 'ky'
|
||||
import { For } from 'solid-js'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { endpointList, setEndpointList, setSelectedEndpoint } from '~/signals'
|
||||
|
||||
const schema = z.object({
|
||||
url: z.string().url().nonempty(),
|
||||
secret: z.string(),
|
||||
})
|
||||
|
||||
export const Setup = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { form } = createForm<z.infer<typeof schema>>({
|
||||
extend: validator({ schema }),
|
||||
async onSubmit(values) {
|
||||
const { hello } = await ky.get(values.url).json<{ hello: string }>()
|
||||
|
||||
if (!hello) {
|
||||
return
|
||||
}
|
||||
|
||||
setEndpointList([
|
||||
{
|
||||
id: uuid(),
|
||||
url: values.url,
|
||||
secret: '',
|
||||
},
|
||||
...endpointList(),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const onRemove = (id: string) =>
|
||||
setEndpointList(endpointList().filter((e) => e.id !== id))
|
||||
|
||||
return (
|
||||
<div class="mx-auto flex w-2/3 flex-col items-center gap-4 py-10">
|
||||
<form class="contents" use:form={form}>
|
||||
<div class="join flex w-full">
|
||||
<input
|
||||
name="url"
|
||||
type="url"
|
||||
class="input join-item input-bordered flex-1"
|
||||
placeholder="http://127.0.0.1:9000"
|
||||
list="defaultEndpoints"
|
||||
/>
|
||||
|
||||
<datalist id="defaultEndpoints">
|
||||
<option value="http://127.0.0.1:9000" />
|
||||
</datalist>
|
||||
|
||||
<input
|
||||
name="secret"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input join-item input-bordered"
|
||||
placeholder="secret"
|
||||
/>
|
||||
|
||||
<button type="submit" class="btn btn-primary join-item uppercase">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<For each={endpointList()}>
|
||||
{({ id, url }) => (
|
||||
<div
|
||||
class="badge badge-info flex w-full cursor-pointer items-center gap-4 py-4"
|
||||
onClick={() => {
|
||||
setSelectedEndpoint(id)
|
||||
navigate('/')
|
||||
}}
|
||||
>
|
||||
{url}
|
||||
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-xs text-white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemove(id)
|
||||
}}
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
44
src/signals/index.ts
Normal file
44
src/signals/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { makePersisted } from '@solid-primitives/storage'
|
||||
import ky from 'ky'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { themes } from '~/constants'
|
||||
|
||||
export const [selectedEndpoint, setSelectedEndpoint] = makePersisted(
|
||||
createSignal(''),
|
||||
{
|
||||
name: 'selectedEndpoint',
|
||||
storage: localStorage,
|
||||
},
|
||||
)
|
||||
|
||||
export const [endpointList, setEndpointList] = makePersisted(
|
||||
createSignal<
|
||||
{
|
||||
id: string
|
||||
url: string
|
||||
secret: string
|
||||
}[]
|
||||
>([]),
|
||||
{ name: 'endpointList', storage: localStorage },
|
||||
)
|
||||
|
||||
export const [curTheme, setCurTheme] = makePersisted(
|
||||
createSignal<(typeof themes)[number]>('business'),
|
||||
{ name: 'theme', storage: localStorage },
|
||||
)
|
||||
|
||||
export const endpoint = () =>
|
||||
endpointList().find(({ id }) => id === selectedEndpoint())
|
||||
|
||||
export const wsEndpointURL = () => endpoint()?.url.replace('http', 'ws')
|
||||
|
||||
export const useRequest = () => {
|
||||
const e = endpoint()
|
||||
|
||||
return ky.create({
|
||||
prefixUrl: e?.url,
|
||||
headers: {
|
||||
Authorization: e?.secret ? `Bearer ${e.secret}` : '',
|
||||
},
|
||||
})
|
||||
}
|
124
src/types.d.ts
vendored
Normal file
124
src/types.d.ts
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
declare module 'solid-js' {
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
form: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Proxy = {
|
||||
name: string
|
||||
type: string
|
||||
all?: string[]
|
||||
extra: Record<string, unknown>
|
||||
history: {
|
||||
time: string
|
||||
delay: number
|
||||
}[]
|
||||
udp: boolean
|
||||
xudp: boolean
|
||||
tfo: boolean
|
||||
now: string
|
||||
}
|
||||
|
||||
export type ProxyProvider = {
|
||||
name: string
|
||||
proxies: {
|
||||
alive: boolean
|
||||
type: string
|
||||
name: string
|
||||
tfo: boolean
|
||||
udp: boolean
|
||||
xudp: boolean
|
||||
id: string
|
||||
extra: Record<string, unknown>
|
||||
history: {
|
||||
time: string
|
||||
delay: number
|
||||
}[]
|
||||
}[]
|
||||
testUrl: string
|
||||
updatedAt: string
|
||||
vehicleType: string
|
||||
}
|
||||
|
||||
export type RuleProvider = {
|
||||
behavior: string
|
||||
format: string
|
||||
name: string
|
||||
ruleCount: number
|
||||
type: string
|
||||
updatedAt: string
|
||||
vehicleType: string
|
||||
}
|
||||
|
||||
export type Connection = {
|
||||
id: string
|
||||
download: number
|
||||
upload: number
|
||||
chains: string[]
|
||||
rule: string
|
||||
rulePayload: string
|
||||
start: string
|
||||
metadata: {
|
||||
network: string
|
||||
type: string
|
||||
destinationIP: string
|
||||
destinationPort: string
|
||||
dnsMode: string
|
||||
host: string
|
||||
inboundIP: string
|
||||
inboundName: string
|
||||
inboundPort: string
|
||||
inboundUser: string
|
||||
process: string
|
||||
processPath: string
|
||||
remoteDestination: string
|
||||
sniffHost: string
|
||||
sourceIP: string
|
||||
sourcePort: string
|
||||
specialProxy: string
|
||||
specialRules: string
|
||||
uid: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
port: number
|
||||
'socks-port': number
|
||||
'redir-port': number
|
||||
'tproxy-port': number
|
||||
'mixed-port': number
|
||||
tun: {
|
||||
enable: boolean
|
||||
device: string
|
||||
stack: string
|
||||
'dns-hijack': null
|
||||
'auto-route': boolean
|
||||
'auto-detect-interface': boolean
|
||||
'file-descriptor': number
|
||||
}
|
||||
'tuic-server': {
|
||||
enable: boolean
|
||||
listen: string
|
||||
certificate: string
|
||||
'private-key': string
|
||||
}
|
||||
'ss-config': string
|
||||
'vmess-config': string
|
||||
authentication: null
|
||||
'allow-lan': boolean
|
||||
'bind-address': string
|
||||
'inbound-tfo': boolean
|
||||
mode: 'rule' | 'global'
|
||||
UnifiedDelay: boolean
|
||||
'log-level': string
|
||||
ipv6: boolean
|
||||
'interface-name': string
|
||||
'geodata-mode': boolean
|
||||
'geodata-loader': string
|
||||
'tcp-concurrent': boolean
|
||||
'find-process-mode': string
|
||||
sniffing: boolean
|
||||
'global-client-fingerprint': boolean
|
||||
}
|
37
tailwind.config.ts
Normal file
37
tailwind.config.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export default {
|
||||
content: ['./src/**/*.{css,tsx}'],
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
'light',
|
||||
'dark',
|
||||
'cupcake',
|
||||
'bumblebee',
|
||||
'emerald',
|
||||
'corporate',
|
||||
'synthwave',
|
||||
'retro',
|
||||
'cyberpunk',
|
||||
'valentine',
|
||||
'halloween',
|
||||
'garden',
|
||||
'forest',
|
||||
'aqua',
|
||||
'lofi',
|
||||
'pastel',
|
||||
'fantasy',
|
||||
'wireframe',
|
||||
'black',
|
||||
'luxury',
|
||||
'dracula',
|
||||
'cmyk',
|
||||
'autumn',
|
||||
'business',
|
||||
'acid',
|
||||
'lemonade',
|
||||
'night',
|
||||
'coffee',
|
||||
'winter',
|
||||
],
|
||||
},
|
||||
}
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
12
vite.config.ts
Normal file
12
vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import devtools from 'solid-devtools/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import solidPlugin from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': '/src',
|
||||
},
|
||||
},
|
||||
plugins: [devtools(), solidPlugin()],
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user