feat: show current page title, closes #474

This commit is contained in:
kunish 2024-10-10 00:49:58 +08:00
parent 443cb251ba
commit 9c76d7ff76
No known key found for this signature in database
GPG Key ID: 647A12B4F782C430
16 changed files with 4729 additions and 2143 deletions

1
auto-imports.d.ts vendored
View File

@ -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']

View File

@ -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>

View File

@ -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"
} }

File diff suppressed because it is too large Load Diff

View 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

View File

@ -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)}>

View File

@ -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',

View File

@ -2,6 +2,7 @@ import { Dict } from '~/i18n/dict'
export default { export default {
add: '添加', add: '添加',
setup: '设置',
overview: '概览', overview: '概览',
proxies: '代理', proxies: '代理',
proxiesSettings: '代理设置', proxiesSettings: '代理设置',

View File

@ -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>

View File

@ -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>
</>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>
</>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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> </>
) )
} }

View File

@ -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> </>
) )
} }