mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Migration: HookStore & Logs to hooks component
This commit is contained in:
parent
a92ee4862f
commit
3d16e6e893
3747
package-lock.json
generated
3747
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,7 @@
|
|||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.0",
|
||||||
"i18next": "^17.0.6",
|
"i18next": "^17.0.6",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
|
"immer": "^3.1.3",
|
||||||
"mobx": "^5.10.1",
|
"mobx": "^5.10.1",
|
||||||
"mobx-react": "^6.1.1",
|
"mobx-react": "^6.1.1",
|
||||||
"mobx-react-router": "^4.0.7",
|
"mobx-react-router": "^4.0.7",
|
||||||
@ -88,6 +89,8 @@
|
|||||||
"react-virtualized": "^9.21.1",
|
"react-virtualized": "^9.21.1",
|
||||||
"terser-webpack-plugin": "^1.3.0",
|
"terser-webpack-plugin": "^1.3.0",
|
||||||
"typescript": "^3.5.2",
|
"typescript": "^3.5.2",
|
||||||
|
"unstated-next": "^1.1.0",
|
||||||
|
"use-immer": "^0.3.2",
|
||||||
"yaml": "^1.6.0"
|
"yaml": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ export function Select (props: SelectProps) {
|
|||||||
const attachmentRef = useRef<HTMLDivElement>()
|
const attachmentRef = useRef<HTMLDivElement>()
|
||||||
const targetRef = useRef<HTMLDivElement>()
|
const targetRef = useRef<HTMLDivElement>()
|
||||||
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
document.addEventListener('click', handleGlobalClick, true)
|
document.addEventListener('click', handleGlobalClick, true)
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,66 +1,58 @@
|
|||||||
import * as React from 'react'
|
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { withTranslation, WithTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Card, Header } from '@components'
|
import { Card, Header } from '@components'
|
||||||
import './style.scss'
|
|
||||||
import { getLogsStreamReader } from '@lib/request'
|
import { getLogsStreamReader } from '@lib/request'
|
||||||
|
import { StreamReader } from '@lib/streamer'
|
||||||
|
import { Log } from '@models/Log'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
interface Log {
|
export default function Logs () {
|
||||||
type: string
|
const listRef = useRef<HTMLUListElement>()
|
||||||
payload: string,
|
const logsRef = useRef<Log[]>([])
|
||||||
time: Date
|
const [logs, setLogs] = useState<Log[]>([])
|
||||||
}
|
const { t } = useTranslation(['Logs'])
|
||||||
|
|
||||||
interface LogsProps extends WithTranslation {}
|
useLayoutEffect(() => {
|
||||||
|
const ul = listRef.current
|
||||||
interface LogsState {
|
|
||||||
logs: Log[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class Logs extends React.Component<LogsProps, LogsState> {
|
|
||||||
state: LogsState = {
|
|
||||||
logs: []
|
|
||||||
}
|
|
||||||
|
|
||||||
private streamReader = null
|
|
||||||
private listRef = React.createRef<HTMLUListElement>()
|
|
||||||
async componentDidMount () {
|
|
||||||
this.streamReader = await getLogsStreamReader()
|
|
||||||
let logs = this.streamReader.buffer()
|
|
||||||
this.setState({ logs }, () => this.scrollToBottom())
|
|
||||||
this.streamReader.subscribe('data', (data) => {
|
|
||||||
logs = [].concat(this.state.logs, data.map(d => ({ ...d, time: new Date() })))
|
|
||||||
this.setState({ logs }, () => this.scrollToBottom())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom = () => {
|
|
||||||
const ul = this.listRef.current
|
|
||||||
ul.scrollTop = ul.scrollHeight
|
ul.scrollTop = ul.scrollHeight
|
||||||
}
|
}, [logsRef.current])
|
||||||
|
|
||||||
render () {
|
useEffect(() => {
|
||||||
const { t } = this.props
|
let streamReader: StreamReader<Log> = null
|
||||||
return (
|
|
||||||
<div className="page">
|
function handleLog (newLogs: Log[]) {
|
||||||
<Header title={ t('title') } />
|
logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() })))
|
||||||
<Card className="logs-card">
|
setLogs(logsRef.current)
|
||||||
<ul className="logs-panel" ref={this.listRef}>
|
}
|
||||||
{
|
|
||||||
this.state.logs.map(
|
void async function () {
|
||||||
(log, index) => (
|
const streamReader = await getLogsStreamReader()
|
||||||
<li key={index}>
|
logsRef.current = streamReader.buffer()
|
||||||
<span className="logs-panel-time">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span>
|
setLogs(logsRef.current)
|
||||||
<span>[{ log.type }] { log.payload }</span>
|
streamReader.subscribe<Log[]>('data', handleLog)
|
||||||
</li>
|
}()
|
||||||
)
|
|
||||||
|
return () => streamReader && streamReader.unsubscribe('data', handleLog)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<Header title={ t('title') } />
|
||||||
|
<Card className="logs-card">
|
||||||
|
<ul className="logs-panel" ref={listRef}>
|
||||||
|
{
|
||||||
|
logs.map(
|
||||||
|
(log, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<span className="logs-panel-time">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span>
|
||||||
|
<span>[{ log.type }] { log.payload }</span>
|
||||||
|
</li>
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
</ul>
|
}
|
||||||
</Card>
|
</ul>
|
||||||
</div>
|
</Card>
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation(['Logs'])(Logs)
|
|
||||||
|
22
src/lib/hook.ts
Normal file
22
src/lib/hook.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Draft } from 'immer'
|
||||||
|
import { useImmer } from 'use-immer'
|
||||||
|
|
||||||
|
export function useObject<T extends object> (initialValue: T) {
|
||||||
|
let [copy, setCopy] = useImmer(initialValue)
|
||||||
|
|
||||||
|
function change<K extends keyof Draft<T>> (key: K, value: Draft<T>[K]) {
|
||||||
|
setCopy(draft => {
|
||||||
|
draft[key] = value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function set<K extends keyof Draft<T>, U extends keyof T> (newValue: T) {
|
||||||
|
setCopy((draft: Draft<T>) => {
|
||||||
|
(draft as any).isTemplate = true
|
||||||
|
for (const key of Object.keys(newValue)) {
|
||||||
|
draft[key as K] = newValue[key as U] as any
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { value: copy, change, set }
|
||||||
|
}
|
@ -2,10 +2,11 @@ import axios, { AxiosInstance } from 'axios'
|
|||||||
import { Partial, getLocalStorageItem } from '@lib/helper'
|
import { Partial, getLocalStorageItem } from '@lib/helper'
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
import { rootStores } from '@lib/createStore'
|
import { rootStores } from '@lib/createStore'
|
||||||
|
import { Log } from '@models/Log'
|
||||||
import { StreamReader } from './streamer'
|
import { StreamReader } from './streamer'
|
||||||
|
|
||||||
let instance: AxiosInstance
|
let instance: AxiosInstance
|
||||||
let logsStreamReader = null
|
let logsStreamReader: StreamReader<Log> = null
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
port: number
|
port: number
|
||||||
|
5
src/models/Log.ts
Normal file
5
src/models/Log.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface Log {
|
||||||
|
type: string
|
||||||
|
payload: string,
|
||||||
|
time: Date
|
||||||
|
}
|
@ -4,19 +4,37 @@ import { Provider } from 'mobx-react'
|
|||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
import { I18nextProvider } from 'react-i18next'
|
import { I18nextProvider } from 'react-i18next'
|
||||||
import { rootStores } from '@lib/createStore'
|
import { rootStores } from '@lib/createStore'
|
||||||
|
import { BaseComponentProps } from '@models/BaseProps'
|
||||||
|
import { APIInfo, Data, ClashXData, ExternalControllerModal } from '@stores'
|
||||||
import App from '@containers/App'
|
import App from '@containers/App'
|
||||||
import i18n from '@i18n'
|
import i18n from '@i18n'
|
||||||
|
|
||||||
|
function Store (props: BaseComponentProps) {
|
||||||
|
return (
|
||||||
|
<APIInfo.Provider>
|
||||||
|
<Data.Provider>
|
||||||
|
<ClashXData.Provider>
|
||||||
|
<ExternalControllerModal.Provider>
|
||||||
|
{ props.children }
|
||||||
|
</ExternalControllerModal.Provider>
|
||||||
|
</ClashXData.Provider>
|
||||||
|
</Data.Provider>
|
||||||
|
</APIInfo.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function renderApp () {
|
export default function renderApp () {
|
||||||
const rootEl = document.getElementById('root')
|
const rootEl = document.getElementById('root')
|
||||||
const AppInstance = (
|
const AppInstance = (
|
||||||
<Provider {...rootStores}>
|
<Store>
|
||||||
<HashRouter>
|
<Provider {...rootStores}>
|
||||||
<I18nextProvider i18n={ i18n }>
|
<HashRouter>
|
||||||
<App />
|
<I18nextProvider i18n={ i18n }>
|
||||||
</I18nextProvider>
|
<App />
|
||||||
</HashRouter>
|
</I18nextProvider>
|
||||||
</Provider>
|
</HashRouter>
|
||||||
|
</Provider>
|
||||||
|
</Store>
|
||||||
)
|
)
|
||||||
|
|
||||||
render(AppInstance, rootEl)
|
render(AppInstance, rootEl)
|
||||||
|
106
src/stores/HookStore.ts
Normal file
106
src/stores/HookStore.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import * as Models from '@models'
|
||||||
|
import { createContainer } from 'unstated-next'
|
||||||
|
import * as API from '@lib/request'
|
||||||
|
import { useObject } from '@lib/hook'
|
||||||
|
import { jsBridge, isClashX } from '@lib/jsBridge'
|
||||||
|
import { setLocalStorageItem, partition } from '@lib/helper'
|
||||||
|
|
||||||
|
function useData () {
|
||||||
|
const { value: data, change } = useObject<Models.Data>({
|
||||||
|
general: {},
|
||||||
|
proxy: [],
|
||||||
|
proxyGroup: [],
|
||||||
|
rules: []
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch () {
|
||||||
|
const [{ data: general }, rawProxies, rules] = await Promise.all([API.getConfig(), API.getProxies(), API.getRules()])
|
||||||
|
|
||||||
|
change('general', {
|
||||||
|
port: general.port,
|
||||||
|
socksPort: general['socks-port'],
|
||||||
|
redirPort: general['redir-port'],
|
||||||
|
mode: general.mode,
|
||||||
|
logLevel: general['log-level'],
|
||||||
|
allowLan: general['allow-lan']
|
||||||
|
})
|
||||||
|
|
||||||
|
const policyGroup = new Set(['Selector', 'URLTest', 'Fallback', 'LoadBalance'])
|
||||||
|
const unUsedProxy = new Set(['DIRECT', 'REJECT', 'GLOBAL'])
|
||||||
|
const proxyList = rawProxies.data.proxies['GLOBAL'] as API.Group
|
||||||
|
const proxies = proxyList.all
|
||||||
|
.filter(key => !unUsedProxy.has(key))
|
||||||
|
.map(key => ({ ...rawProxies.data.proxies[key], name: key }))
|
||||||
|
const [proxy, groups] = partition(proxies, proxy => !policyGroup.has(proxy.type))
|
||||||
|
|
||||||
|
change('proxy', proxy as API.Proxy[])
|
||||||
|
change('proxyGroup', groups as API.Group[])
|
||||||
|
change('rules', rules.data.rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, fetch }
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAPIInfo () {
|
||||||
|
const { value: data, set } = useObject<Models.APIInfo>({
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: '9090',
|
||||||
|
secret: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch () {
|
||||||
|
if (isClashX()) {
|
||||||
|
const apiInfo = await jsBridge.getAPIInfo()
|
||||||
|
set({ hostname: apiInfo.host, port: apiInfo.port, secret: apiInfo.secret })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const info = await API.getExternalControllerConfig()
|
||||||
|
set({ ...info })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update (info: Models.APIInfo) {
|
||||||
|
const { hostname, port, secret } = info
|
||||||
|
setLocalStorageItem('externalControllerAddr', hostname)
|
||||||
|
setLocalStorageItem('externalControllerPort', port)
|
||||||
|
setLocalStorageItem('secret', secret)
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, fetch, update }
|
||||||
|
}
|
||||||
|
|
||||||
|
function useClashXData () {
|
||||||
|
const { value: data, set } = useObject<Models.ClashXData>({
|
||||||
|
startAtLogin: false,
|
||||||
|
systemProxy: false
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch () {
|
||||||
|
const startAtLogin = await jsBridge.getStartAtLogin()
|
||||||
|
const systemProxy = await jsBridge.isSystemProxySet()
|
||||||
|
|
||||||
|
set({ startAtLogin, systemProxy })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, fetch }
|
||||||
|
}
|
||||||
|
|
||||||
|
function useExternalControllerModal () {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
function show () {
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidden () {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { visible, show, hidden }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Data = createContainer(useData)
|
||||||
|
export const APIInfo = createContainer(useAPIInfo)
|
||||||
|
export const ClashXData = createContainer(useClashXData)
|
||||||
|
export const ExternalControllerModal = createContainer(useExternalControllerModal)
|
@ -1,2 +1,3 @@
|
|||||||
export * from './ConfigStore'
|
export * from './ConfigStore'
|
||||||
export * from './RouterStore'
|
export * from './RouterStore'
|
||||||
|
export * from './HookStore'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user