mirror of
https://github.com/MetaCubeX/metacubexd.git
synced 2024-11-26 22:34:02 +08:00
feat: show current page title, closes #474
This commit is contained in:
parent
443cb251ba
commit
9c76d7ff76
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@ -3,6 +3,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
export { }
|
export { }
|
||||||
declare global {
|
declare global {
|
||||||
const $DEVCOMP: (typeof import('solid-js'))['$DEVCOMP']
|
const $DEVCOMP: (typeof import('solid-js'))['$DEVCOMP']
|
||||||
|
@ -9,9 +9,8 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
||||||
|
|
||||||
<title>metacubexd</title>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"@solid-primitives/storage": "^4.2.1",
|
"@solid-primitives/storage": "^4.2.1",
|
||||||
"@solid-primitives/timer": "^1.3.10",
|
"@solid-primitives/timer": "^1.3.10",
|
||||||
"@solid-primitives/websocket": "^1.2.2",
|
"@solid-primitives/websocket": "^1.2.2",
|
||||||
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.14.7",
|
"@solidjs/router": "^0.14.7",
|
||||||
"@tabler/icons-solidjs": "^3.19.0",
|
"@tabler/icons-solidjs": "^3.19.0",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
@ -44,7 +45,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"commitlint": "^19.5.0",
|
"commitlint": "^19.5.0",
|
||||||
"daisyui": "^4.12.12",
|
"daisyui": "^4.12.13",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@ -73,5 +74,5 @@
|
|||||||
"vite-plugin-solid": "^2.10.2",
|
"vite-plugin-solid": "^2.10.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.10.0"
|
"packageManager": "pnpm@9.12.1"
|
||||||
}
|
}
|
||||||
|
5255
pnpm-lock.yaml
5255
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
6
src/components/DocumentTitle.tsx
Normal file
6
src/components/DocumentTitle.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Title } from '@solidjs/meta'
|
||||||
|
import { ParentComponent } from 'solid-js'
|
||||||
|
|
||||||
|
export default (({ children }) => {
|
||||||
|
return <Title>{children} - MetaCubeXD</Title>
|
||||||
|
}) as ParentComponent
|
@ -119,7 +119,7 @@ export const Header = () => {
|
|||||||
<div class="drawer-side">
|
<div class="drawer-side">
|
||||||
<label for="navs" class="drawer-overlay" />
|
<label for="navs" class="drawer-overlay" />
|
||||||
|
|
||||||
<ul class="menu min-h-full w-2/5 gap-2 rounded-r-box bg-base-300 pt-20">
|
<ul class="min-w-2/5 menu min-h-full gap-2 rounded-r-box bg-base-300 pt-20">
|
||||||
<For each={navs()}>
|
<For each={navs()}>
|
||||||
{({ href, name, icon }) => (
|
{({ href, name, icon }) => (
|
||||||
<li onClick={() => setOpenedDrawer(false)}>
|
<li onClick={() => setOpenedDrawer(false)}>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
|
setup: 'Setup',
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
proxies: 'Proxies',
|
proxies: 'Proxies',
|
||||||
proxiesSettings: 'Proxies Settings',
|
proxiesSettings: 'Proxies Settings',
|
||||||
|
@ -2,6 +2,7 @@ import { Dict } from '~/i18n/dict'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
|
setup: '设置',
|
||||||
overview: '概览',
|
overview: '概览',
|
||||||
proxies: '代理',
|
proxies: '代理',
|
||||||
proxiesSettings: '代理设置',
|
proxiesSettings: '代理设置',
|
||||||
|
23
src/main.tsx
23
src/main.tsx
@ -1,6 +1,7 @@
|
|||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
import '~/index.css'
|
import '~/index.css'
|
||||||
|
|
||||||
|
import { MetaProvider } from '@solidjs/meta'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
@ -24,16 +25,18 @@ dayjs.extend(relativeTime)
|
|||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<I18nProvider locale={locale()}>
|
<I18nProvider locale={locale()}>
|
||||||
<HashRouter root={App}>
|
<MetaProvider>
|
||||||
<Route path={ROUTES.Setup} component={Setup} />
|
<HashRouter root={App}>
|
||||||
<Route path="*" component={Overview} />
|
<Route path={ROUTES.Setup} component={Setup} />
|
||||||
<Route path={ROUTES.Overview} component={Overview} />
|
<Route path="*" component={Overview} />
|
||||||
<Route path={ROUTES.Proxies} component={Proxies} />
|
<Route path={ROUTES.Overview} component={Overview} />
|
||||||
<Route path={ROUTES.Rules} component={Rules} />
|
<Route path={ROUTES.Proxies} component={Proxies} />
|
||||||
<Route path={ROUTES.Conns} component={Connections} />
|
<Route path={ROUTES.Rules} component={Rules} />
|
||||||
<Route path={ROUTES.Log} component={Logs} />
|
<Route path={ROUTES.Conns} component={Connections} />
|
||||||
<Route path={ROUTES.Config} component={Config} />
|
<Route path={ROUTES.Log} component={Logs} />
|
||||||
</HashRouter>
|
<Route path={ROUTES.Config} component={Config} />
|
||||||
|
</HashRouter>
|
||||||
|
</MetaProvider>
|
||||||
|
|
||||||
<Toaster position="bottom-center" />
|
<Toaster position="bottom-center" />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
upgradingUI,
|
upgradingUI,
|
||||||
} from '~/apis'
|
} from '~/apis'
|
||||||
import { Button, ConfigTitle } from '~/components'
|
import { Button, ConfigTitle } from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { LANG, ROUTES, themes } from '~/constants'
|
import { LANG, ROUTES, themes } from '~/constants'
|
||||||
import { locale, setLocale, useI18n } from '~/i18n'
|
import { locale, setLocale, useI18n } from '~/i18n'
|
||||||
import {
|
import {
|
||||||
@ -540,24 +541,28 @@ export default () => {
|
|||||||
updateBackendVersion()
|
updateBackendVersion()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mx-auto flex max-w-screen-md flex-col gap-4">
|
<>
|
||||||
<Show when={!isSingBox()}>
|
<DocumentTitle>{t('config')}</DocumentTitle>
|
||||||
<ConfigTitle withDivider>{t('dnsQuery')}</ConfigTitle>
|
|
||||||
|
|
||||||
<DNSQueryForm />
|
<div class="mx-auto flex max-w-screen-md flex-col gap-4">
|
||||||
</Show>
|
<Show when={!isSingBox()}>
|
||||||
|
<ConfigTitle withDivider>{t('dnsQuery')}</ConfigTitle>
|
||||||
|
|
||||||
<ConfigTitle withDivider>{t('coreConfig')}</ConfigTitle>
|
<DNSQueryForm />
|
||||||
|
</Show>
|
||||||
|
|
||||||
<ConfigForm />
|
<ConfigTitle withDivider>{t('coreConfig')}</ConfigTitle>
|
||||||
|
|
||||||
<ConfigTitle withDivider>{t('xdConfig')}</ConfigTitle>
|
<ConfigForm />
|
||||||
|
|
||||||
<ConfigForXd />
|
<ConfigTitle withDivider>{t('xdConfig')}</ConfigTitle>
|
||||||
|
|
||||||
<ConfigTitle withDivider>{t('version')}</ConfigTitle>
|
<ConfigForXd />
|
||||||
|
|
||||||
<Versions backendVersion={backendVersion} />
|
<ConfigTitle withDivider>{t('version')}</ConfigTitle>
|
||||||
</div>
|
|
||||||
|
<Versions backendVersion={backendVersion} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
ConnectionsSettingsModal,
|
ConnectionsSettingsModal,
|
||||||
ConnectionsTableDetailsModal,
|
ConnectionsTableDetailsModal,
|
||||||
} from '~/components'
|
} from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants'
|
import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants'
|
||||||
import { useI18n } from '~/i18n'
|
import { useI18n } from '~/i18n'
|
||||||
import {
|
import {
|
||||||
@ -355,231 +356,238 @@ export default () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-full flex-col gap-2">
|
<>
|
||||||
<div class="flex w-full flex-wrap items-center gap-2">
|
<DocumentTitle>{t('connections')}</DocumentTitle>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="tabs-boxed tabs gap-2">
|
<div class="flex h-full flex-col gap-2">
|
||||||
<Index each={tabs()}>
|
<div class="flex w-full flex-wrap items-center gap-2">
|
||||||
{(tab) => (
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<div class="tabs-boxed tabs gap-2">
|
||||||
class={twMerge(
|
<Index each={tabs()}>
|
||||||
activeTab() === tab().type && 'tab-active',
|
{(tab) => (
|
||||||
'tab-sm sm:tab-md tab gap-2 px-2',
|
<button
|
||||||
)}
|
class={twMerge(
|
||||||
onClick={() => setActiveTab(tab().type)}
|
activeTab() === tab().type && 'tab-active',
|
||||||
>
|
'tab-sm sm:tab-md tab gap-2 px-2',
|
||||||
<span>{tab().name}</span>
|
)}
|
||||||
<div class="badge badge-sm">{tab().count}</div>
|
onClick={() => setActiveTab(tab().type)}
|
||||||
</button>
|
>
|
||||||
)}
|
<span>{tab().name}</span>
|
||||||
</Index>
|
<div class="badge badge-sm">{tab().count}</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 hidden lg:inline-block">
|
||||||
|
{t('quickFilter')}:
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle"
|
||||||
|
checked={enableQuickFilter()}
|
||||||
|
onChange={(e) => setEnableQuickFilter(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-primary select-sm w-full max-w-full flex-1"
|
||||||
|
onChange={(e) => setSourceIPFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{t('all')}</option>
|
||||||
|
|
||||||
|
<Index
|
||||||
|
each={uniq(
|
||||||
|
allConnections().map(({ metadata: { sourceIP } }) => {
|
||||||
|
const tagged = clientSourceIPTags().find(
|
||||||
|
(tag) => tag.sourceIP === sourceIP,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tagged ? tagged.tagName : sourceIP
|
||||||
|
}),
|
||||||
|
).sort()}
|
||||||
|
>
|
||||||
|
{(sourceIP) => <option value={sourceIP()}>{sourceIP()}</option>}
|
||||||
|
</Index>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="join flex flex-1 items-center md:flex-1">
|
||||||
<span class="mr-2 hidden lg:inline-block">{t('quickFilter')}:</span>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="search"
|
||||||
class="toggle"
|
class="input input-sm join-item input-primary min-w-0 flex-1"
|
||||||
checked={enableQuickFilter()}
|
placeholder={t('search')}
|
||||||
onChange={(e) => setEnableQuickFilter(e.target.checked)}
|
onInput={(e) => setGlobalFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn btn-primary join-item btn-sm"
|
||||||
|
onClick={() => setPaused((paused) => !paused)}
|
||||||
|
icon={paused() ? <IconPlayerPlay /> : <IconPlayerPause />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn btn-primary join-item btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (table.getState().globalFilter) {
|
||||||
|
table
|
||||||
|
.getFilteredRowModel()
|
||||||
|
.rows.forEach(({ original }) =>
|
||||||
|
closeSingleConnectionAPI(original.id),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
closeAllConnectionsAPI()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={<IconX />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn btn-primary join-item btn-sm"
|
||||||
|
onClick={() => connectionsSettingsModalRef?.showModal()}
|
||||||
|
icon={<IconSettings />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<select
|
<div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300">
|
||||||
class="select select-bordered select-primary select-sm w-full max-w-full flex-1"
|
<table
|
||||||
onChange={(e) => setSourceIPFilter(e.target.value)}
|
class={twMerge(
|
||||||
|
tableSizeClassName(connectionsTableSize()),
|
||||||
|
'table table-zebra relative rounded-none',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<option value="">{t('all')}</option>
|
<thead class="sticky top-0 z-10 h-8">
|
||||||
|
<For each={table.getHeaderGroups()}>
|
||||||
<Index
|
{(headerGroup) => (
|
||||||
each={uniq(
|
<tr>
|
||||||
allConnections().map(({ metadata: { sourceIP } }) => {
|
<For each={headerGroup.headers}>
|
||||||
const tagged = clientSourceIPTags().find(
|
{(header) => (
|
||||||
(tag) => tag.sourceIP === sourceIP,
|
<th class="bg-base-200">
|
||||||
)
|
<div class={twMerge('flex items-center gap-2')}>
|
||||||
|
{header.column.getCanGroup() ? (
|
||||||
return tagged ? tagged.tagName : sourceIP
|
<button
|
||||||
}),
|
class="cursor-pointer"
|
||||||
).sort()}
|
onClick={header.column.getToggleGroupingHandler()}
|
||||||
>
|
>
|
||||||
{(sourceIP) => <option value={sourceIP()}>{sourceIP()}</option>}
|
{header.column.getIsGrouped() ? (
|
||||||
</Index>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="join flex flex-1 items-center md:flex-1">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
class="input input-sm join-item input-primary min-w-0 flex-1"
|
|
||||||
placeholder={t('search')}
|
|
||||||
onInput={(e) => setGlobalFilter(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn btn-primary join-item btn-sm"
|
|
||||||
onClick={() => setPaused((paused) => !paused)}
|
|
||||||
icon={paused() ? <IconPlayerPlay /> : <IconPlayerPause />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn btn-primary join-item btn-sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (table.getState().globalFilter) {
|
|
||||||
table
|
|
||||||
.getFilteredRowModel()
|
|
||||||
.rows.forEach(({ original }) =>
|
|
||||||
closeSingleConnectionAPI(original.id),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
closeAllConnectionsAPI()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
icon={<IconX />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn btn-primary join-item btn-sm"
|
|
||||||
onClick={() => connectionsSettingsModalRef?.showModal()}
|
|
||||||
icon={<IconSettings />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300">
|
|
||||||
<table
|
|
||||||
class={twMerge(
|
|
||||||
tableSizeClassName(connectionsTableSize()),
|
|
||||||
'table table-zebra relative rounded-none',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<thead class="sticky top-0 z-10 h-8">
|
|
||||||
<For each={table.getHeaderGroups()}>
|
|
||||||
{(headerGroup) => (
|
|
||||||
<tr>
|
|
||||||
<For each={headerGroup.headers}>
|
|
||||||
{(header) => (
|
|
||||||
<th class="bg-base-200">
|
|
||||||
<div class={twMerge('flex items-center gap-2')}>
|
|
||||||
{header.column.getCanGroup() ? (
|
|
||||||
<button
|
|
||||||
class="cursor-pointer"
|
|
||||||
onClick={header.column.getToggleGroupingHandler()}
|
|
||||||
>
|
|
||||||
{header.column.getIsGrouped() ? (
|
|
||||||
<IconZoomOutFilled size={18} />
|
|
||||||
) : (
|
|
||||||
<IconZoomInFilled size={18} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={twMerge(
|
|
||||||
header.column.getCanSort() &&
|
|
||||||
'cursor-pointer select-none',
|
|
||||||
'flex-1',
|
|
||||||
)}
|
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
|
||||||
>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{
|
|
||||||
asc: <IconSortAscending />,
|
|
||||||
desc: <IconSortDescending />,
|
|
||||||
}[header.column.getIsSorted() as string] ?? null}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For each={table.getRowModel().rows}>
|
|
||||||
{(row) => (
|
|
||||||
<tr class="hover:!bg-primary hover:text-primary-content">
|
|
||||||
<For each={row.getVisibleCells()}>
|
|
||||||
{(cell) => {
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
class="py-2"
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const value = cell.renderValue() as null | string
|
|
||||||
value && writeClipboard(value).catch(() => {})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cell.getIsGrouped() ? (
|
|
||||||
<button
|
|
||||||
class={twMerge(
|
|
||||||
row.getCanExpand()
|
|
||||||
? 'cursor-pointer'
|
|
||||||
: 'cursor-normal',
|
|
||||||
'flex items-center gap-2',
|
|
||||||
)}
|
|
||||||
onClick={row.getToggleExpandedHandler()}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{row.getIsExpanded() ? (
|
|
||||||
<IconZoomOutFilled size={18} />
|
<IconZoomOutFilled size={18} />
|
||||||
) : (
|
) : (
|
||||||
<IconZoomInFilled size={18} />
|
<IconZoomInFilled size={18} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div
|
||||||
{flexRender(
|
class={twMerge(
|
||||||
cell.column.columnDef.cell,
|
header.column.getCanSort() &&
|
||||||
cell.getContext(),
|
'cursor-pointer select-none',
|
||||||
|
'flex-1',
|
||||||
|
)}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{
|
||||||
|
asc: <IconSortAscending />,
|
||||||
|
desc: <IconSortDescending />,
|
||||||
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<For each={table.getRowModel().rows}>
|
||||||
|
{(row) => (
|
||||||
|
<tr class="hover:!bg-primary hover:text-primary-content">
|
||||||
|
<For each={row.getVisibleCells()}>
|
||||||
|
{(cell) => {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
class="py-2"
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const value = cell.renderValue() as null | string
|
||||||
|
|
||||||
|
if (value) writeClipboard(value).catch(() => {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cell.getIsGrouped() ? (
|
||||||
|
<button
|
||||||
|
class={twMerge(
|
||||||
|
row.getCanExpand()
|
||||||
|
? 'cursor-pointer'
|
||||||
|
: 'cursor-normal',
|
||||||
|
'flex items-center gap-2',
|
||||||
)}
|
)}
|
||||||
</div>
|
onClick={row.getToggleExpandedHandler()}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{row.getIsExpanded() ? (
|
||||||
|
<IconZoomOutFilled size={18} />
|
||||||
|
) : (
|
||||||
|
<IconZoomInFilled size={18} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>({row.subRows.length})</div>
|
<div>
|
||||||
</button>
|
{flexRender(
|
||||||
) : cell.getIsAggregated() ? (
|
cell.column.columnDef.cell,
|
||||||
flexRender(
|
cell.getContext(),
|
||||||
cell.column.columnDef.aggregatedCell ??
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>({row.subRows.length})</div>
|
||||||
|
</button>
|
||||||
|
) : cell.getIsAggregated() ? (
|
||||||
|
flexRender(
|
||||||
|
cell.column.columnDef.aggregatedCell ??
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)
|
||||||
|
) : cell.getIsPlaceholder() ? null : (
|
||||||
|
flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext(),
|
cell.getContext(),
|
||||||
)
|
)
|
||||||
) : cell.getIsPlaceholder() ? null : (
|
)}
|
||||||
flexRender(
|
</td>
|
||||||
cell.column.columnDef.cell,
|
)
|
||||||
cell.getContext(),
|
}}
|
||||||
)
|
</For>
|
||||||
)}
|
</tr>
|
||||||
</td>
|
)}
|
||||||
)
|
</For>
|
||||||
}}
|
</tbody>
|
||||||
</For>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
)}
|
|
||||||
</For>
|
<ConnectionsSettingsModal
|
||||||
</tbody>
|
ref={(el) => (connectionsSettingsModalRef = el)}
|
||||||
</table>
|
order={connectionsTableColumnOrder()}
|
||||||
|
visible={connectionsTableColumnVisibility()}
|
||||||
|
onOrderChange={(data) => setConnectionsTableColumnOrder(data)}
|
||||||
|
onVisibleChange={(data) =>
|
||||||
|
setConnectionsTableColumnVisibility({ ...data })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConnectionsTableDetailsModal
|
||||||
|
ref={(el) => (connectionsDetailsModalRef = el)}
|
||||||
|
selectedConnectionID={selectedConnectionID()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<ConnectionsSettingsModal
|
|
||||||
ref={(el) => (connectionsSettingsModalRef = el)}
|
|
||||||
order={connectionsTableColumnOrder()}
|
|
||||||
visible={connectionsTableColumnVisibility()}
|
|
||||||
onOrderChange={(data) => setConnectionsTableColumnOrder(data)}
|
|
||||||
onVisibleChange={(data) =>
|
|
||||||
setConnectionsTableColumnVisibility({ ...data })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConnectionsTableDetailsModal
|
|
||||||
ref={(el) => (connectionsDetailsModalRef = el)}
|
|
||||||
selectedConnectionID={selectedConnectionID()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
} from '@tanstack/solid-table'
|
} from '@tanstack/solid-table'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import { Button, LogsSettingsModal } from '~/components'
|
import { Button, LogsSettingsModal } from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { LOG_LEVEL } from '~/constants'
|
import { LOG_LEVEL } from '~/constants'
|
||||||
import { useI18n } from '~/i18n'
|
import { useI18n } from '~/i18n'
|
||||||
import { endpoint, logsTableSize, tableSizeClassName } from '~/signals'
|
import { endpoint, logsTableSize, tableSizeClassName } from '~/signals'
|
||||||
@ -120,99 +121,104 @@ export default () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-full flex-col gap-2">
|
<>
|
||||||
<div class="join w-full">
|
<DocumentTitle>{t('logs')}</DocumentTitle>
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
class="input input-sm join-item input-primary flex-1 flex-shrink-0"
|
|
||||||
placeholder={t('search')}
|
|
||||||
onInput={(e) => setGlobalFilter(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<div class="flex h-full flex-col gap-2">
|
||||||
class="btn-primary join-item btn-sm"
|
<div class="join w-full">
|
||||||
onClick={() => setPaused((paused) => !paused)}
|
<input
|
||||||
icon={paused() ? <IconPlayerPlay /> : <IconPlayerPause />}
|
type="search"
|
||||||
/>
|
class="input input-sm join-item input-primary flex-1 flex-shrink-0"
|
||||||
<Button
|
placeholder={t('search')}
|
||||||
class="btn-primary join-item btn-sm"
|
onInput={(e) => setGlobalFilter(e.target.value)}
|
||||||
onClick={() => logsSettingsModalRef?.showModal()}
|
/>
|
||||||
icon={<IconSettings />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300">
|
<Button
|
||||||
<table
|
class="btn-primary join-item btn-sm"
|
||||||
class={twMerge(
|
onClick={() => setPaused((paused) => !paused)}
|
||||||
tableSizeClassName(logsTableSize()),
|
icon={paused() ? <IconPlayerPlay /> : <IconPlayerPause />}
|
||||||
'table relative rounded-none',
|
/>
|
||||||
)}
|
<Button
|
||||||
>
|
class="btn-primary join-item btn-sm"
|
||||||
<thead class="sticky top-0 z-10">
|
onClick={() => logsSettingsModalRef?.showModal()}
|
||||||
<Index each={table.getHeaderGroups()}>
|
icon={<IconSettings />}
|
||||||
{(keyedHeaderGroup) => {
|
/>
|
||||||
const headerGroup = keyedHeaderGroup()
|
</div>
|
||||||
|
|
||||||
return (
|
<div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300">
|
||||||
<tr>
|
<table
|
||||||
<Index each={headerGroup.headers}>
|
class={twMerge(
|
||||||
{(keyedHeader) => {
|
tableSizeClassName(logsTableSize()),
|
||||||
const header = keyedHeader()
|
'table relative rounded-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<thead class="sticky top-0 z-10">
|
||||||
|
<Index each={table.getHeaderGroups()}>
|
||||||
|
{(keyedHeaderGroup) => {
|
||||||
|
const headerGroup = keyedHeaderGroup()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th class="bg-base-200">
|
<tr>
|
||||||
<div class="flex items-center">
|
<Index each={headerGroup.headers}>
|
||||||
<div
|
{(keyedHeader) => {
|
||||||
class={twMerge(
|
const header = keyedHeader()
|
||||||
header.column.getCanSort() &&
|
|
||||||
'cursor-pointer select-none',
|
return (
|
||||||
'flex-1',
|
<th class="bg-base-200">
|
||||||
)}
|
<div class="flex items-center">
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
<div
|
||||||
>
|
class={twMerge(
|
||||||
{flexRender(
|
header.column.getCanSort() &&
|
||||||
header.column.columnDef.header,
|
'cursor-pointer select-none',
|
||||||
header.getContext(),
|
'flex-1',
|
||||||
)}
|
)}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{
|
||||||
|
asc: <IconSortAscending />,
|
||||||
|
desc: <IconSortDescending />,
|
||||||
|
}[header.column.getIsSorted() as string] ??
|
||||||
|
null}
|
||||||
</div>
|
</div>
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Index>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Index>
|
||||||
|
</thead>
|
||||||
|
|
||||||
{{
|
<tbody>
|
||||||
asc: <IconSortAscending />,
|
<For each={table.getRowModel().rows}>
|
||||||
desc: <IconSortDescending />,
|
{(row) => (
|
||||||
}[header.column.getIsSorted() as string] ?? null}
|
<tr class="hover:!bg-primary hover:text-primary-content">
|
||||||
</div>
|
<For each={row.getVisibleCells()}>
|
||||||
</th>
|
{(cell) => (
|
||||||
)
|
<td class="py-2">
|
||||||
}}
|
{flexRender(
|
||||||
</Index>
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)}
|
||||||
}}
|
</For>
|
||||||
</Index>
|
</tbody>
|
||||||
</thead>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tbody>
|
<LogsSettingsModal ref={(el) => (logsSettingsModalRef = el)} />
|
||||||
<For each={table.getRowModel().rows}>
|
|
||||||
{(row) => (
|
|
||||||
<tr class="hover:!bg-primary hover:text-primary-content">
|
|
||||||
<For each={row.getVisibleCells()}>
|
|
||||||
{(cell) => (
|
|
||||||
<td class="py-2">
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<LogsSettingsModal ref={(el) => (logsSettingsModalRef = el)} />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import byteSize from 'byte-size'
|
|||||||
import { merge } from 'lodash'
|
import { merge } from 'lodash'
|
||||||
import { SolidApexCharts } from 'solid-apexcharts'
|
import { SolidApexCharts } from 'solid-apexcharts'
|
||||||
import type { JSX, ParentComponent } from 'solid-js'
|
import type { JSX, ParentComponent } from 'solid-js'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { CHART_MAX_XAXIS, DEFAULT_CHART_OPTIONS } from '~/constants'
|
import { CHART_MAX_XAXIS, DEFAULT_CHART_OPTIONS } from '~/constants'
|
||||||
import { useI18n } from '~/i18n'
|
import { useI18n } from '~/i18n'
|
||||||
import { endpoint, latestConnectionMsg, useWsRequest } from '~/signals'
|
import { endpoint, latestConnectionMsg, useWsRequest } from '~/signals'
|
||||||
@ -87,53 +88,57 @@ export default () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2 lg:h-full">
|
<>
|
||||||
<div class="stats stats-vertical w-full flex-shrink-0 grid-cols-2 bg-gradient-to-br from-primary to-secondary shadow lg:stats-horizontal lg:flex">
|
<DocumentTitle>{t('overview')}</DocumentTitle>
|
||||||
<TrafficWidget label={t('upload')}>
|
|
||||||
{byteSize(traffic()?.up || 0).toString()}/s
|
|
||||||
</TrafficWidget>
|
|
||||||
|
|
||||||
<TrafficWidget label={t('download')}>
|
<div class="flex flex-col gap-2 lg:h-full">
|
||||||
{byteSize(traffic()?.down || 0).toString()}/s
|
<div class="stats stats-vertical w-full flex-shrink-0 grid-cols-2 bg-gradient-to-br from-primary to-secondary shadow lg:stats-horizontal lg:flex">
|
||||||
</TrafficWidget>
|
<TrafficWidget label={t('upload')}>
|
||||||
|
{byteSize(traffic()?.up || 0).toString()}/s
|
||||||
|
</TrafficWidget>
|
||||||
|
|
||||||
<TrafficWidget label={t('uploadTotal')}>
|
<TrafficWidget label={t('download')}>
|
||||||
{byteSize(latestConnectionMsg()?.uploadTotal || 0).toString()}
|
{byteSize(traffic()?.down || 0).toString()}/s
|
||||||
</TrafficWidget>
|
</TrafficWidget>
|
||||||
|
|
||||||
<TrafficWidget label={t('downloadTotal')}>
|
<TrafficWidget label={t('uploadTotal')}>
|
||||||
{byteSize(latestConnectionMsg()?.downloadTotal || 0).toString()}
|
{byteSize(latestConnectionMsg()?.uploadTotal || 0).toString()}
|
||||||
</TrafficWidget>
|
</TrafficWidget>
|
||||||
|
|
||||||
<TrafficWidget label={t('activeConnections')}>
|
<TrafficWidget label={t('downloadTotal')}>
|
||||||
{latestConnectionMsg()?.connections?.length || 0}
|
{byteSize(latestConnectionMsg()?.downloadTotal || 0).toString()}
|
||||||
</TrafficWidget>
|
</TrafficWidget>
|
||||||
|
|
||||||
<TrafficWidget label={t('memoryUsage')}>
|
<TrafficWidget label={t('activeConnections')}>
|
||||||
{byteSize(memory()?.inuse || 0).toString()}
|
{latestConnectionMsg()?.connections?.length || 0}
|
||||||
</TrafficWidget>
|
</TrafficWidget>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 rounded-box bg-base-300 py-4 lg:flex-row">
|
<TrafficWidget label={t('memoryUsage')}>
|
||||||
<div class="flex-1">
|
{byteSize(memory()?.inuse || 0).toString()}
|
||||||
<SolidApexCharts
|
</TrafficWidget>
|
||||||
type="area"
|
|
||||||
options={trafficChartOptions()}
|
|
||||||
series={trafficChartSeries()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
|
||||||
<SolidApexCharts
|
|
||||||
type="line"
|
|
||||||
options={memoryChartOptions()}
|
|
||||||
series={memoryChartSeries()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="footer mx-auto mt-4 block rounded-box bg-neutral p-4 text-center text-lg font-bold text-neutral-content">
|
<div class="flex flex-col gap-2 rounded-box bg-base-300 py-4 lg:flex-row">
|
||||||
{endpoint()?.url}
|
<div class="flex-1">
|
||||||
</footer>
|
<SolidApexCharts
|
||||||
</div>
|
type="area"
|
||||||
|
options={trafficChartOptions()}
|
||||||
|
series={trafficChartSeries()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<SolidApexCharts
|
||||||
|
type="line"
|
||||||
|
options={memoryChartOptions()}
|
||||||
|
series={memoryChartSeries()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer mx-auto mt-4 block rounded-box bg-neutral p-4 text-center text-lg font-bold text-neutral-content">
|
||||||
|
{endpoint()?.url}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
ProxyNodePreview,
|
ProxyNodePreview,
|
||||||
SubscriptionInfo,
|
SubscriptionInfo,
|
||||||
} from '~/components'
|
} from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import {
|
import {
|
||||||
filterProxiesByAvailability,
|
filterProxiesByAvailability,
|
||||||
sortProxiesByOrderingType,
|
sortProxiesByOrderingType,
|
||||||
@ -125,257 +126,269 @@ export default () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-full flex-col gap-2">
|
<>
|
||||||
<div class="flex items-center gap-2">
|
<DocumentTitle>{t('proxies')}</DocumentTitle>
|
||||||
<div class="tabs-boxed tabs gap-2">
|
|
||||||
<For each={tabs()}>
|
|
||||||
{(tab) => (
|
|
||||||
<button
|
|
||||||
class={twMerge(
|
|
||||||
activeTab() === tab.type && 'tab-active',
|
|
||||||
'tab-sm sm:tab-md tab gap-2 px-2',
|
|
||||||
)}
|
|
||||||
onClick={() => setActiveTab(tab.type)}
|
|
||||||
>
|
|
||||||
<span>{tab.name}</span>
|
|
||||||
<div class="badge badge-sm">{tab.count}</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={activeTab() === ActiveTab.proxyProviders}>
|
<div class="flex h-full flex-col gap-2">
|
||||||
<Button
|
<div class="flex items-center gap-2">
|
||||||
class="btn btn-circle btn-sm"
|
<div class="tabs-boxed tabs gap-2">
|
||||||
disabled={isAllProviderUpdating()}
|
<For each={tabs()}>
|
||||||
onClick={(e) => onUpdateAllProviderClick(e)}
|
{(tab) => (
|
||||||
icon={
|
<button
|
||||||
<IconReload
|
class={twMerge(
|
||||||
class={twMerge(
|
activeTab() === tab.type && 'tab-active',
|
||||||
isAllProviderUpdating() && 'animate-spin text-success',
|
'tab-sm sm:tab-md tab gap-2 px-2',
|
||||||
)}
|
)}
|
||||||
/>
|
onClick={() => setActiveTab(tab.type)}
|
||||||
}
|
>
|
||||||
/>
|
<span>{tab.name}</span>
|
||||||
</Show>
|
<div class="badge badge-sm">{tab.count}</div>
|
||||||
|
</button>
|
||||||
<div class="ml-auto">
|
)}
|
||||||
<Button
|
|
||||||
class="btn-circle btn-primary btn-sm"
|
|
||||||
onClick={() => proxiesSettingsModalRef?.showModal()}
|
|
||||||
icon={<IconSettings />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
|
||||||
<Show when={activeTab() === ActiveTab.proxies}>
|
|
||||||
<div
|
|
||||||
class={twMerge(
|
|
||||||
'grid grid-cols-1 place-items-start gap-2',
|
|
||||||
renderProxiesInTwoColumns() ? 'sm:grid-cols-2' : 'sm:grid-cols-1',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<For each={renderProxies()}>
|
|
||||||
{(proxyGroup) => {
|
|
||||||
const sortedProxyNames = createMemo(() =>
|
|
||||||
filterProxiesByAvailability(
|
|
||||||
sortProxiesByOrderingType(
|
|
||||||
proxyGroup.all ?? [],
|
|
||||||
proxiesOrderingType(),
|
|
||||||
),
|
|
||||||
hideUnAvailableProxies(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const title = (
|
|
||||||
<>
|
|
||||||
<div class="flex items-center justify-between pr-8">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Show when={proxyGroup.icon}>
|
|
||||||
<img
|
|
||||||
src={proxyGroup.icon}
|
|
||||||
style={{
|
|
||||||
height: `${iconHeight()}px`,
|
|
||||||
'margin-right': `${iconMarginRight()}px`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<span>{proxyGroup.name}</span>
|
|
||||||
<div class="badge badge-sm ml-2">
|
|
||||||
{proxyGroup.all?.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn-circle btn-sm"
|
|
||||||
disabled={
|
|
||||||
proxyGroupLatencyTestingMap()[proxyGroup.name]
|
|
||||||
}
|
|
||||||
onClick={(e) =>
|
|
||||||
onProxyGroupLatencyTestClick(e, proxyGroup.name)
|
|
||||||
}
|
|
||||||
icon={
|
|
||||||
<IconBrandSpeedtest
|
|
||||||
class={twMerge(
|
|
||||||
proxyGroupLatencyTestingMap()[proxyGroup.name] &&
|
|
||||||
'animate-pulse text-success',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between text-sm text-slate-500">
|
|
||||||
<span>
|
|
||||||
{proxyGroup.type}{' '}
|
|
||||||
{proxyGroup.now?.length > 0 && ` :: ${proxyGroup.now}`}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{byteSize(
|
|
||||||
speedGroupByName()[proxyGroup.name] ?? 0,
|
|
||||||
).toString()}
|
|
||||||
/s
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={!collapsedMap()[proxyGroup.name]}>
|
|
||||||
<ProxyNodePreview
|
|
||||||
proxyNameList={sortedProxyNames()}
|
|
||||||
now={proxyGroup.now}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapse
|
|
||||||
isOpen={collapsedMap()[proxyGroup.name]}
|
|
||||||
title={title}
|
|
||||||
onCollapse={(val) =>
|
|
||||||
setCollapsedMapByKey(proxyGroup.name, val)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={sortedProxyNames()}>
|
|
||||||
{(proxyName) => (
|
|
||||||
<ProxyNodeCard
|
|
||||||
proxyName={proxyName}
|
|
||||||
isSelected={proxyGroup.now === proxyName}
|
|
||||||
onClick={() =>
|
|
||||||
void selectProxyInGroup(proxyGroup, proxyName)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Collapse>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={activeTab() === ActiveTab.proxyProviders}>
|
<Show when={activeTab() === ActiveTab.proxyProviders}>
|
||||||
<div
|
<Button
|
||||||
class={twMerge(
|
class="btn btn-circle btn-sm"
|
||||||
'grid grid-cols-1 place-items-start gap-2',
|
disabled={isAllProviderUpdating()}
|
||||||
renderProxiesInTwoColumns() ? 'sm:grid-cols-2' : 'sm:grid-cols-1',
|
onClick={(e) => onUpdateAllProviderClick(e)}
|
||||||
)}
|
icon={
|
||||||
>
|
<IconReload
|
||||||
<For each={proxyProviders()}>
|
class={twMerge(
|
||||||
{(proxyProvider) => {
|
isAllProviderUpdating() && 'animate-spin text-success',
|
||||||
const sortedProxyNames = createMemo(() =>
|
)}
|
||||||
sortProxiesByOrderingType(
|
/>
|
||||||
proxyProvider.proxies.map((i) => i.name) ?? [],
|
}
|
||||||
proxiesOrderingType(),
|
/>
|
||||||
),
|
</Show>
|
||||||
)
|
|
||||||
|
|
||||||
const title = (
|
<div class="ml-auto">
|
||||||
<>
|
<Button
|
||||||
<div class="flex items-center justify-between pr-8">
|
class="btn-circle btn-primary btn-sm"
|
||||||
<div class="flex items-center gap-2">
|
onClick={() => proxiesSettingsModalRef?.showModal()}
|
||||||
<span>{proxyProvider.name}</span>
|
icon={<IconSettings />}
|
||||||
<div class="badge badge-sm">
|
/>
|
||||||
{proxyProvider.proxies.length}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<Button
|
<Show when={activeTab() === ActiveTab.proxies}>
|
||||||
class="btn btn-circle btn-sm"
|
<div
|
||||||
disabled={updatingMap()[proxyProvider.name]}
|
class={twMerge(
|
||||||
onClick={(e) =>
|
'grid grid-cols-1 place-items-start gap-2',
|
||||||
onUpdateProxyProviderClick(e, proxyProvider.name)
|
renderProxiesInTwoColumns()
|
||||||
}
|
? 'sm:grid-cols-2'
|
||||||
icon={
|
: 'sm:grid-cols-1',
|
||||||
<IconReload
|
)}
|
||||||
class={twMerge(
|
>
|
||||||
updatingMap()[proxyProvider.name] &&
|
<For each={renderProxies()}>
|
||||||
'animate-spin text-success',
|
{(proxyGroup) => {
|
||||||
)}
|
const sortedProxyNames = createMemo(() =>
|
||||||
|
filterProxiesByAvailability(
|
||||||
|
sortProxiesByOrderingType(
|
||||||
|
proxyGroup.all ?? [],
|
||||||
|
proxiesOrderingType(),
|
||||||
|
),
|
||||||
|
hideUnAvailableProxies(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<>
|
||||||
|
<div class="flex items-center justify-between pr-8">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Show when={proxyGroup.icon}>
|
||||||
|
<img
|
||||||
|
src={proxyGroup.icon}
|
||||||
|
style={{
|
||||||
|
height: `${iconHeight()}px`,
|
||||||
|
'margin-right': `${iconMarginRight()}px`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
</Show>
|
||||||
/>
|
<span>{proxyGroup.name}</span>
|
||||||
|
<div class="badge badge-sm ml-2">
|
||||||
|
{proxyGroup.all?.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="btn btn-circle btn-sm"
|
class="btn-circle btn-sm"
|
||||||
disabled={
|
disabled={
|
||||||
proxyProviderLatencyTestingMap()[proxyProvider.name]
|
proxyGroupLatencyTestingMap()[proxyGroup.name]
|
||||||
}
|
}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
onProxyProviderLatencyTestClick(
|
onProxyGroupLatencyTestClick(e, proxyGroup.name)
|
||||||
e,
|
|
||||||
proxyProvider.name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
<IconBrandSpeedtest
|
<IconBrandSpeedtest
|
||||||
class={twMerge(
|
class={twMerge(
|
||||||
proxyProviderLatencyTestingMap()[
|
proxyGroupLatencyTestingMap()[
|
||||||
proxyProvider.name
|
proxyGroup.name
|
||||||
] && 'animate-pulse text-success',
|
] && 'animate-pulse text-success',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<SubscriptionInfo
|
<div class="flex items-center justify-between text-sm text-slate-500">
|
||||||
subscriptionInfo={proxyProvider.subscriptionInfo}
|
<span>
|
||||||
/>
|
{proxyGroup.type}{' '}
|
||||||
|
{proxyGroup.now?.length > 0 &&
|
||||||
|
` :: ${proxyGroup.now}`}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{byteSize(
|
||||||
|
speedGroupByName()[proxyGroup.name] ?? 0,
|
||||||
|
).toString()}
|
||||||
|
/s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-slate-500">
|
<Show when={!collapsedMap()[proxyGroup.name]}>
|
||||||
{proxyProvider.vehicleType} :: {t('updated')}{' '}
|
<ProxyNodePreview
|
||||||
{formatTimeFromNow(proxyProvider.updatedAt)}
|
proxyNameList={sortedProxyNames()}
|
||||||
</div>
|
now={proxyGroup.now}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
<Show when={!collapsedMap()[proxyProvider.name]}>
|
return (
|
||||||
<ProxyNodePreview proxyNameList={sortedProxyNames()} />
|
<Collapse
|
||||||
</Show>
|
isOpen={collapsedMap()[proxyGroup.name]}
|
||||||
</>
|
title={title}
|
||||||
)
|
onCollapse={(val) =>
|
||||||
|
setCollapsedMapByKey(proxyGroup.name, val)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={sortedProxyNames()}>
|
||||||
|
{(proxyName) => (
|
||||||
|
<ProxyNodeCard
|
||||||
|
proxyName={proxyName}
|
||||||
|
isSelected={proxyGroup.now === proxyName}
|
||||||
|
onClick={() =>
|
||||||
|
void selectProxyInGroup(proxyGroup, proxyName)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Collapse>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
return (
|
<Show when={activeTab() === ActiveTab.proxyProviders}>
|
||||||
<Collapse
|
<div
|
||||||
isOpen={collapsedMap()[proxyProvider.name]}
|
class={twMerge(
|
||||||
title={title}
|
'grid grid-cols-1 place-items-start gap-2',
|
||||||
onCollapse={(val) =>
|
renderProxiesInTwoColumns()
|
||||||
setCollapsedMapByKey(proxyProvider.name, val)
|
? 'sm:grid-cols-2'
|
||||||
}
|
: 'sm:grid-cols-1',
|
||||||
>
|
)}
|
||||||
<For each={sortedProxyNames()}>
|
>
|
||||||
{(proxyName) => <ProxyNodeCard proxyName={proxyName} />}
|
<For each={proxyProviders()}>
|
||||||
</For>
|
{(proxyProvider) => {
|
||||||
</Collapse>
|
const sortedProxyNames = createMemo(() =>
|
||||||
)
|
sortProxiesByOrderingType(
|
||||||
}}
|
proxyProvider.proxies.map((i) => i.name) ?? [],
|
||||||
</For>
|
proxiesOrderingType(),
|
||||||
</div>
|
),
|
||||||
</Show>
|
)
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<>
|
||||||
|
<div class="flex items-center justify-between pr-8">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{proxyProvider.name}</span>
|
||||||
|
<div class="badge badge-sm">
|
||||||
|
{proxyProvider.proxies.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
class="btn btn-circle btn-sm"
|
||||||
|
disabled={updatingMap()[proxyProvider.name]}
|
||||||
|
onClick={(e) =>
|
||||||
|
onUpdateProxyProviderClick(e, proxyProvider.name)
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
<IconReload
|
||||||
|
class={twMerge(
|
||||||
|
updatingMap()[proxyProvider.name] &&
|
||||||
|
'animate-spin text-success',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn btn-circle btn-sm"
|
||||||
|
disabled={
|
||||||
|
proxyProviderLatencyTestingMap()[
|
||||||
|
proxyProvider.name
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onClick={(e) =>
|
||||||
|
onProxyProviderLatencyTestClick(
|
||||||
|
e,
|
||||||
|
proxyProvider.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
<IconBrandSpeedtest
|
||||||
|
class={twMerge(
|
||||||
|
proxyProviderLatencyTestingMap()[
|
||||||
|
proxyProvider.name
|
||||||
|
] && 'animate-pulse text-success',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubscriptionInfo
|
||||||
|
subscriptionInfo={proxyProvider.subscriptionInfo}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-sm text-slate-500">
|
||||||
|
{proxyProvider.vehicleType} :: {t('updated')}{' '}
|
||||||
|
{formatTimeFromNow(proxyProvider.updatedAt)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!collapsedMap()[proxyProvider.name]}>
|
||||||
|
<ProxyNodePreview proxyNameList={sortedProxyNames()} />
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
isOpen={collapsedMap()[proxyProvider.name]}
|
||||||
|
title={title}
|
||||||
|
onCollapse={(val) =>
|
||||||
|
setCollapsedMapByKey(proxyProvider.name, val)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={sortedProxyNames()}>
|
||||||
|
{(proxyName) => <ProxyNodeCard proxyName={proxyName} />}
|
||||||
|
</For>
|
||||||
|
</Collapse>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProxiesSettingsModal ref={(el) => (proxiesSettingsModalRef = el)} />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<ProxiesSettingsModal ref={(el) => (proxiesSettingsModalRef = el)} />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { createVirtualizer } from '@tanstack/solid-virtual'
|
|||||||
import { matchSorter } from 'match-sorter'
|
import { matchSorter } from 'match-sorter'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import { Button } from '~/components'
|
import { Button } from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { useStringBooleanMap } from '~/helpers'
|
import { useStringBooleanMap } from '~/helpers'
|
||||||
import { useI18n } from '~/i18n'
|
import { useI18n } from '~/i18n'
|
||||||
import { endpoint, formatTimeFromNow, useRules } from '~/signals'
|
import { endpoint, formatTimeFromNow, useRules } from '~/signals'
|
||||||
@ -124,147 +125,155 @@ export default () => {
|
|||||||
const ruleProviderVirtualizerItems = ruleProviderVirtualizer.getVirtualItems()
|
const ruleProviderVirtualizerItems = ruleProviderVirtualizer.getVirtualItems()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-full flex-col gap-2">
|
<>
|
||||||
<div class="flex items-center gap-2">
|
<DocumentTitle>{t('rules')}</DocumentTitle>
|
||||||
<div class="tabs-boxed tabs gap-2">
|
|
||||||
<For each={tabs()}>
|
<div class="flex h-full flex-col gap-2">
|
||||||
{(tab) => (
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<div class="tabs-boxed tabs gap-2">
|
||||||
class={twMerge(
|
<For each={tabs()}>
|
||||||
activeTab() === tab.type && 'tab-active',
|
{(tab) => (
|
||||||
'tab-sm md:tab-md tab gap-2 px-2',
|
<button
|
||||||
)}
|
class={twMerge(
|
||||||
onClick={() => setActiveTab(tab.type)}
|
activeTab() === tab.type && 'tab-active',
|
||||||
>
|
'tab-sm md:tab-md tab gap-2 px-2',
|
||||||
<span>{tab.name}</span>
|
)}
|
||||||
<div class="badge badge-sm">{tab.count}</div>
|
onClick={() => setActiveTab(tab.type)}
|
||||||
</button>
|
>
|
||||||
)}
|
<span>{tab.name}</span>
|
||||||
</For>
|
<div class="badge badge-sm">{tab.count}</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={activeTab() === ActiveTab.ruleProviders}>
|
||||||
|
<Button
|
||||||
|
class="btn btn-circle btn-sm"
|
||||||
|
disabled={allProviderIsUpdating()}
|
||||||
|
onClick={(e) => onUpdateAllProviderClick(e)}
|
||||||
|
icon={
|
||||||
|
<IconReload
|
||||||
|
class={twMerge(
|
||||||
|
allProviderIsUpdating() && 'animate-spin text-success',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={activeTab() === ActiveTab.ruleProviders}>
|
<input
|
||||||
<Button
|
class="input input-sm input-bordered input-primary"
|
||||||
class="btn btn-circle btn-sm"
|
placeholder={t('search')}
|
||||||
disabled={allProviderIsUpdating()}
|
value={globalFilter()}
|
||||||
onClick={(e) => onUpdateAllProviderClick(e)}
|
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
|
||||||
icon={
|
/>
|
||||||
<IconReload
|
|
||||||
class={twMerge(
|
|
||||||
allProviderIsUpdating() && 'animate-spin text-success',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
<div
|
||||||
class="input input-sm input-bordered input-primary"
|
ref={(ref) => (scrollElementRef = ref)}
|
||||||
placeholder={t('search')}
|
class="flex-1 overflow-y-auto"
|
||||||
value={globalFilter()}
|
>
|
||||||
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
|
<Show when={activeTab() === ActiveTab.rules}>
|
||||||
/>
|
<div
|
||||||
|
class="relative"
|
||||||
|
style={{ height: `${ruleVirtualizer.getTotalSize()}px` }}
|
||||||
|
>
|
||||||
|
{ruleVirtualizerItems.map((virtualizerItem) => {
|
||||||
|
const rule = filteredRules().find(
|
||||||
|
(rule) => getRuleItemKey(rule) === virtualizerItem.key,
|
||||||
|
)!
|
||||||
|
|
||||||
<div
|
return (
|
||||||
ref={(ref) => (scrollElementRef = ref)}
|
<div
|
||||||
class="flex-1 overflow-y-auto"
|
ref={(el) =>
|
||||||
>
|
onMount(() => ruleVirtualizer.measureElement(el))
|
||||||
<Show when={activeTab() === ActiveTab.rules}>
|
}
|
||||||
<div
|
data-index={virtualizerItem.index}
|
||||||
class="relative"
|
class="absolute inset-x-0 top-0 pb-2 last:pb-0"
|
||||||
style={{ height: `${ruleVirtualizer.getTotalSize()}px` }}
|
style={{
|
||||||
>
|
transform: `translateY(${virtualizerItem.start}px)`,
|
||||||
{ruleVirtualizerItems.map((virtualizerItem) => {
|
}}
|
||||||
const rule = filteredRules().find(
|
>
|
||||||
(rule) => getRuleItemKey(rule) === virtualizerItem.key,
|
<div class="card card-bordered card-compact bg-base-200 p-4">
|
||||||
)!
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="break-all">{rule.payload}</span>
|
||||||
|
|
||||||
return (
|
<Show when={rule.size !== -1}>
|
||||||
<div
|
<div class="badge badge-sm">{rule.size}</div>
|
||||||
ref={(el) =>
|
</Show>
|
||||||
onMount(() => ruleVirtualizer.measureElement(el))
|
</div>
|
||||||
}
|
|
||||||
data-index={virtualizerItem.index}
|
|
||||||
class="absolute inset-x-0 top-0 pb-2 last:pb-0"
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${virtualizerItem.start}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="card card-bordered card-compact bg-base-200 p-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="break-all">{rule.payload}</span>
|
|
||||||
|
|
||||||
<Show when={rule.size !== -1}>
|
<div class="text-xs text-slate-500">
|
||||||
<div class="badge badge-sm">{rule.size}</div>
|
{rule.type} :: {rule.proxy}
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-xs text-slate-500">
|
|
||||||
{rule.type} :: {rule.proxy}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={activeTab() === ActiveTab.ruleProviders}>
|
<Show when={activeTab() === ActiveTab.ruleProviders}>
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
style={{ height: `${ruleProviderVirtualizer.getTotalSize()}px` }}
|
style={{ height: `${ruleProviderVirtualizer.getTotalSize()}px` }}
|
||||||
>
|
>
|
||||||
{ruleProviderVirtualizerItems.map((virtualizerItem) => {
|
{ruleProviderVirtualizerItems.map((virtualizerItem) => {
|
||||||
const ruleProvider = ruleProviders().find(
|
const ruleProvider = ruleProviders().find(
|
||||||
(ruleProvider) =>
|
(ruleProvider) =>
|
||||||
getRuleProviderItemKey(ruleProvider) === virtualizerItem.key,
|
getRuleProviderItemKey(ruleProvider) ===
|
||||||
)!
|
virtualizerItem.key,
|
||||||
|
)!
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(el) =>
|
ref={(el) =>
|
||||||
onMount(() => ruleProviderVirtualizer.measureElement(el))
|
onMount(() => ruleProviderVirtualizer.measureElement(el))
|
||||||
}
|
}
|
||||||
data-index={virtualizerItem.index}
|
data-index={virtualizerItem.index}
|
||||||
class="absolute inset-x-0 top-0 pb-2 last:pb-0"
|
class="absolute inset-x-0 top-0 pb-2 last:pb-0"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateY(${virtualizerItem.start}px)`,
|
transform: `translateY(${virtualizerItem.start}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="card card-bordered card-compact bg-base-200 p-4">
|
<div class="card card-bordered card-compact bg-base-200 p-4">
|
||||||
<div class="flex items-center gap-2 pr-8">
|
<div class="flex items-center gap-2 pr-8">
|
||||||
<span class="break-all">{ruleProvider.name}</span>
|
<span class="break-all">{ruleProvider.name}</span>
|
||||||
<div class="badge badge-sm">{ruleProvider.ruleCount}</div>
|
<div class="badge badge-sm">
|
||||||
|
{ruleProvider.ruleCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-slate-500">
|
||||||
|
{ruleProvider.vehicleType} / {ruleProvider.behavior} /
|
||||||
|
{t('updated')}{' '}
|
||||||
|
{formatTimeFromNow(ruleProvider.updatedAt)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn-circle btn-sm absolute right-2 top-2 mr-2 h-4"
|
||||||
|
disabled={updatingMap()[ruleProvider.name]}
|
||||||
|
onClick={(e) =>
|
||||||
|
onUpdateProviderClick(e, ruleProvider.name)
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
<IconReload
|
||||||
|
class={twMerge(
|
||||||
|
updatingMap()[ruleProvider.name] &&
|
||||||
|
'animate-spin text-success',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-slate-500">
|
|
||||||
{ruleProvider.vehicleType} / {ruleProvider.behavior} /
|
|
||||||
{t('updated')} {formatTimeFromNow(ruleProvider.updatedAt)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn-circle btn-sm absolute right-2 top-2 mr-2 h-4"
|
|
||||||
disabled={updatingMap()[ruleProvider.name]}
|
|
||||||
onClick={(e) =>
|
|
||||||
onUpdateProviderClick(e, ruleProvider.name)
|
|
||||||
}
|
|
||||||
icon={
|
|
||||||
<IconReload
|
|
||||||
class={twMerge(
|
|
||||||
updatingMap()[ruleProvider.name] &&
|
|
||||||
'animate-spin text-success',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { toast } from 'solid-toast'
|
|||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { Button } from '~/components'
|
import { Button } from '~/components'
|
||||||
|
import DocumentTitle from '~/components/DocumentTitle'
|
||||||
import { transformEndpointURL } from '~/helpers'
|
import { transformEndpointURL } from '~/helpers'
|
||||||
import { useI18n } from '~/i18n'
|
import { useI18n } from '~/i18n'
|
||||||
import {
|
import {
|
||||||
@ -140,71 +141,75 @@ export default () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mx-auto flex max-w-screen-sm flex-col items-center gap-4 py-10">
|
<>
|
||||||
<form class="contents" use:form={form}>
|
<DocumentTitle>{t('setup')}</DocumentTitle>
|
||||||
<div class="flex w-full flex-col gap-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">{t('endpointURL')}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
<div class="mx-auto flex max-w-screen-sm flex-col items-center gap-4 py-10">
|
||||||
name="url"
|
<form class="contents" use:form={form}>
|
||||||
type="url"
|
<div class="flex w-full flex-col gap-4">
|
||||||
class="input input-bordered w-full"
|
<div class="flex-1">
|
||||||
placeholder="http(s)://{hostname}:{port}"
|
<label class="label">
|
||||||
list="defaultEndpoints"
|
<span class="label-text">{t('endpointURL')}</span>
|
||||||
/>
|
</label>
|
||||||
|
|
||||||
<datalist id="defaultEndpoints">
|
<input
|
||||||
<option value="http://127.0.0.1:9090" />
|
name="url"
|
||||||
<Show when={window.location.origin !== 'http://127.0.0.1:9090'}>
|
type="url"
|
||||||
<option value={window.location.origin} />
|
class="input input-bordered w-full"
|
||||||
</Show>
|
placeholder="http(s)://{hostname}:{port}"
|
||||||
</datalist>
|
list="defaultEndpoints"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div class="flex-1">
|
<datalist id="defaultEndpoints">
|
||||||
<label class="label">
|
<option value="http://127.0.0.1:9090" />
|
||||||
<span class="label-text">{t('secret')}</span>
|
<Show when={window.location.origin !== 'http://127.0.0.1:9090'}>
|
||||||
</label>
|
<option value={window.location.origin} />
|
||||||
|
</Show>
|
||||||
<input
|
</datalist>
|
||||||
name="secret"
|
|
||||||
type="password"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="secret"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" class="btn-primary uppercase">
|
|
||||||
{t('add')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="grid w-full grid-cols-2 gap-4">
|
|
||||||
<For each={endpointList()}>
|
|
||||||
{({ id, url }) => (
|
|
||||||
<div
|
|
||||||
class="badge badge-info flex w-full cursor-pointer items-center justify-between gap-4 py-4"
|
|
||||||
onClick={() => onEndpointSelect(id)}
|
|
||||||
>
|
|
||||||
<span class="truncate">{url}</span>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="btn-circle btn-ghost btn-xs text-white"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onRemove(id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconX />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</For>
|
<div class="flex-1">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{t('secret')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="secret"
|
||||||
|
type="password"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="secret"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" class="btn-primary uppercase">
|
||||||
|
{t('add')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="grid w-full grid-cols-2 gap-4">
|
||||||
|
<For each={endpointList()}>
|
||||||
|
{({ id, url }) => (
|
||||||
|
<div
|
||||||
|
class="badge badge-info flex w-full cursor-pointer items-center justify-between gap-4 py-4"
|
||||||
|
onClick={() => onEndpointSelect(id)}
|
||||||
|
>
|
||||||
|
<span class="truncate">{url}</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="btn-circle btn-ghost btn-xs text-white"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onRemove(id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user