chore: initial commit

This commit is contained in:
kunish 2023-08-24 04:20:53 +08:00
commit b8c6c2fa7a
No known key found for this signature in database
GPG Key ID: 647A12B4F782C430
25 changed files with 3286 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"semi": false,
"singleQuote": true
}

34
README.md Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

71
src/App.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
export const Config = () => {
return <div>config</div>
}

162
src/pages/Connections.tsx Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
export const Rules = () => {
return <div>rules</div>
}

99
src/pages/Setup.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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()],
})