feat(modals): add modal component (#268)

* feat: add modal component

* feat: add modal component

* fix: proxies page arrow button problem

* fix: button radius problem

* feat: add modal component

* feat: add modal component

---------

Signed-off-by: Alpha <61853980+AlphaGHX@users.noreply.github.com>
This commit is contained in:
Alpha 2023-09-23 20:24:43 +08:00 committed by GitHub
parent 04c8d2524b
commit eca9a160cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 161 additions and 175 deletions

View File

@ -33,7 +33,12 @@ export const Button: ParentComponent<
<div class="loading loading-spinner" /> <div class="loading loading-spinner" />
</Show> </Show>
<span class="truncate" classList={{ 'flex-1': !local.icon }}> <span
class="truncate rounded-none"
classList={{
'flex-1': !local.icon,
}}
>
{props.icon || props.children} {props.icon || props.children}
</span> </span>
</button> </button>

View File

@ -28,7 +28,7 @@ export const Collapse: ParentComponent<Props> = (props) => {
<div <div
class={twMerge( class={twMerge(
getCollapseClassName(), getCollapseClassName(),
'collapse collapse-arrow select-none overflow-visible border-secondary bg-base-200 drop-shadow-md', 'collapse collapse-arrow select-none overflow-visible border-secondary bg-base-200 shadow-md',
)} )}
> >
<div <div

View File

@ -1,6 +1,6 @@
import { createForm } from '@felte/solid' import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-zod' import { validator } from '@felte/validator-zod'
import { IconX } from '@tabler/icons-solidjs' import { IconNetwork, IconX } from '@tabler/icons-solidjs'
import type { import type {
DragEventHandler, DragEventHandler,
Draggable, Draggable,
@ -18,12 +18,11 @@ import {
import { uniq } from 'lodash' import { uniq } from 'lodash'
import { Component, For, Index, Show, createSignal } from 'solid-js' import { Component, For, Index, Show, createSignal } from 'solid-js'
import { z } from 'zod' import { z } from 'zod'
import { Button, ConfigTitle } from '~/components' import { Button, ConfigTitle, Modal } from '~/components'
import { import {
CONNECTIONS_TABLE_ACCESSOR_KEY, CONNECTIONS_TABLE_ACCESSOR_KEY,
CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER, CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER,
CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY, CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY,
MODAL,
TAILWINDCSS_SIZE, TAILWINDCSS_SIZE,
} from '~/constants' } from '~/constants'
import { useI18n } from '~/i18n' import { useI18n } from '~/i18n'
@ -107,13 +106,14 @@ const TagClientSourceIPWithNameForm: Component = () => {
} }
export const ConnectionsSettingsModal = (props: { export const ConnectionsSettingsModal = (props: {
ref?: (el: HTMLDialogElement) => void
order: ConnectionsTableColumnOrder order: ConnectionsTableColumnOrder
visible: ConnectionsTableColumnVisibility visible: ConnectionsTableColumnVisibility
onOrderChange: (value: ConnectionsTableColumnOrder) => void onOrderChange: (value: ConnectionsTableColumnOrder) => void
onVisibleChange: (value: ConnectionsTableColumnVisibility) => void onVisibleChange: (value: ConnectionsTableColumnVisibility) => void
}) => { }) => {
const modalID = MODAL.CONNECTIONS_SETTINGS
const [t] = useI18n() const [t] = useI18n()
const [activeKey, setActiveKey] = const [activeKey, setActiveKey] =
createSignal<CONNECTIONS_TABLE_ACCESSOR_KEY | null>(null) createSignal<CONNECTIONS_TABLE_ACCESSOR_KEY | null>(null)
@ -179,25 +179,23 @@ export const ConnectionsSettingsModal = (props: {
} }
return ( return (
<dialog id={modalID} class="modal modal-bottom sm:modal-middle"> <Modal
<div ref={(el) => props.ref?.(el)}
class="modal-box flex flex-col gap-4" icon={<IconNetwork size={24} />}
onContextMenu={(e) => e.preventDefault()} title={t('connectionsSettings')}
> action={
<div class="sticky top-0 z-50 flex items-center justify-end"> <Button
<Button class="btn-neutral btn-sm"
class="btn-circle btn-sm" onClick={() => {
onClick={() => { props.onOrderChange(CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER)
const modal = document.querySelector( props.onVisibleChange(CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY)
`#${modalID}`, }}
) as HTMLDialogElement | null >
{t('reset')}
modal?.close() </Button>
}} }
icon={<IconX size={20} />} >
/> <div class="flex flex-col gap-4">
</div>
<div> <div>
<ConfigTitle withDivider>{t('tableSize')}</ConfigTitle> <ConfigTitle withDivider>{t('tableSize')}</ConfigTitle>
@ -267,23 +265,7 @@ export const ConnectionsSettingsModal = (props: {
</DragOverlay> </DragOverlay>
</DragDropProvider> </DragDropProvider>
</div> </div>
<div class="modal-action">
<Button
class="btn-neutral btn-sm ml-auto mt-4 block"
onClick={() => {
props.onOrderChange(CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER)
props.onVisibleChange(CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY)
}}
>
{t('reset')}
</Button>
</div>
</div> </div>
</Modal>
<form method="dialog" class="modal-backdrop">
<button />
</form>
</dialog>
) )
} }

View File

@ -1,50 +1,34 @@
import { IconX } from '@tabler/icons-solidjs' import { IconNetwork } from '@tabler/icons-solidjs'
import { Component, Show } from 'solid-js' import { Component, Show } from 'solid-js'
import { MODAL } from '~/constants' import { Modal } from '~/components'
import { useI18n } from '~/i18n'
import { allConnections } from '~/signals' import { allConnections } from '~/signals'
import { Button } from './Button'
export const ConnectionsTableDetailsModal: Component<{ export const ConnectionsTableDetailsModal: Component<{
ref?: (el: HTMLDialogElement) => void
selectedConnectionID?: string selectedConnectionID?: string
}> = (props) => { }> = (props) => {
const modalID = MODAL.CONNECTIONS_TABLE_DETAILS const [t] = useI18n()
return ( return (
<dialog id={modalID} class="modal modal-bottom sm:modal-middle"> <Modal
<div class="modal-box"> ref={(el) => props.ref?.(el)}
<div class="sticky top-0 z-50 flex items-center justify-end"> icon={<IconNetwork size={24} />}
<Button title={t('connectionsDetails')}
class="btn-circle btn-sm" >
onClick={() => { <Show when={props.selectedConnectionID}>
const modal = document.querySelector( <pre>
`#${modalID}`, <code>
) as HTMLDialogElement | null {JSON.stringify(
allConnections().find(
modal?.close() ({ id }) => id === props.selectedConnectionID,
}} ),
> null,
<IconX size={20} /> 2,
</Button> )}
</div> </code>
</pre>
<Show when={props.selectedConnectionID}> </Show>
<pre> </Modal>
<code>
{JSON.stringify(
allConnections().find(
({ id }) => id === props.selectedConnectionID,
),
null,
2,
)}
</code>
</pre>
</Show>
</div>
<form method="dialog" class="modal-backdrop">
<button />
</form>
</dialog>
) )
} }

View File

@ -1,10 +1,9 @@
import { IconX } from '@tabler/icons-solidjs' import { IconFileStack } from '@tabler/icons-solidjs'
import { For } from 'solid-js' import { Component, For } from 'solid-js'
import { Button, ConfigTitle } from '~/components' import { ConfigTitle, Modal } from '~/components'
import { import {
LOGS_TABLE_MAX_ROWS_LIST, LOGS_TABLE_MAX_ROWS_LIST,
LOG_LEVEL, LOG_LEVEL,
MODAL,
TAILWINDCSS_SIZE, TAILWINDCSS_SIZE,
} from '~/constants' } from '~/constants'
import { useI18n } from '~/i18n' import { useI18n } from '~/i18n'
@ -17,27 +16,18 @@ import {
setLogsTableSize, setLogsTableSize,
} from '~/signals' } from '~/signals'
export const LogsSettingsModal = () => { export const LogsSettingsModal: Component<{
const modalID = MODAL.LOGS_SETTINGS ref?: (el: HTMLDialogElement) => void
}> = (props) => {
const [t] = useI18n() const [t] = useI18n()
return ( return (
<dialog id={modalID} class="modal modal-bottom sm:modal-middle"> <Modal
<div class="modal-box flex flex-col gap-4"> ref={(el) => props.ref?.(el)}
<div class="sticky top-0 z-50 flex items-center justify-end"> icon={<IconFileStack size={24} />}
<Button title={t('logsSettings')}
class="btn-circle btn-sm" >
onClick={() => { <div class="flex flex-col gap-4">
const modal = document.querySelector(
`#${modalID}`,
) as HTMLDialogElement | null
modal?.close()
}}
icon={<IconX size={20} />}
/>
</div>
<div> <div>
<ConfigTitle withDivider>{t('tableSize')}</ConfigTitle> <ConfigTitle withDivider>{t('tableSize')}</ConfigTitle>
@ -90,10 +80,6 @@ export const LogsSettingsModal = () => {
</select> </select>
</div> </div>
</div> </div>
</Modal>
<form method="dialog" class="modal-backdrop">
<button />
</form>
</dialog>
) )
} }

53
src/components/Modal.tsx Normal file
View File

@ -0,0 +1,53 @@
import { IconX } from '@tabler/icons-solidjs'
import { JSX, ParentComponent, Show, children } from 'solid-js'
import { twMerge } from 'tailwind-merge'
import { Button } from '~/components'
type Props = {
ref?: (el: HTMLDialogElement) => void
icon?: JSX.Element
title?: JSX.Element
action?: JSX.Element
}
const actionClass =
'sticky bottom-0 z-50 flex items-center justify-end bg-base-100 bg-opacity-80 p-6 backdrop-blur'
export const Modal: ParentComponent<Props> = (props) => {
let dialogRef: HTMLDialogElement | undefined
return (
<dialog
ref={(el) => (dialogRef = el) && props.ref?.(el)}
class="modal modal-bottom sm:modal-middle"
>
<div class="modal-box p-0" onContextMenu={(e) => e.preventDefault()}>
<div class={twMerge(actionClass, 'top-0 justify-between')}>
<div class="flex items-center gap-4 text-xl font-bold">
{props.icon}
<span>{props.title}</span>
</div>
<Button
class="btn-circle btn-sm"
onClick={() => {
dialogRef?.close()
}}
icon={<IconX size={20} />}
/>
</div>
<div class="p-6 pt-3">{children(() => props.children)()}</div>
<Show when={props.action}>
<div class={actionClass}>
<div class="flex justify-end gap-2">{props.action}</div>
</div>
</Show>
</div>
<form method="dialog" class="modal-backdrop">
<button />
</form>
</dialog>
)
}

View File

@ -1,7 +1,7 @@
import { IconX } from '@tabler/icons-solidjs' import { IconGlobe } from '@tabler/icons-solidjs'
import { For } from 'solid-js' import { Component, For } from 'solid-js'
import { Button, ConfigTitle } from '~/components' import { ConfigTitle, Modal } from '~/components'
import { MODAL, PROXIES_ORDERING_TYPE, PROXIES_PREVIEW_TYPE } from '~/constants' import { PROXIES_ORDERING_TYPE, PROXIES_PREVIEW_TYPE } from '~/constants'
import { useI18n } from '~/i18n' import { useI18n } from '~/i18n'
import { import {
autoCloseConns, autoCloseConns,
@ -18,27 +18,18 @@ import {
urlForLatencyTest, urlForLatencyTest,
} from '~/signals' } from '~/signals'
export const ProxiesSettingsModal = () => { export const ProxiesSettingsModal: Component<{
const modalID = MODAL.PROXIES_SETTINGS ref?: (el: HTMLDialogElement) => void
}> = (props) => {
const [t] = useI18n() const [t] = useI18n()
return ( return (
<dialog id={modalID} class="modal modal-bottom sm:modal-middle"> <Modal
<div class="modal-box flex flex-col gap-4"> ref={(el) => props.ref?.(el)}
<div class="sticky top-0 z-50 flex items-center justify-end"> icon={<IconGlobe size={24} />}
<Button title={t('proxiesSettings')}
class="btn-circle btn-sm" >
onClick={() => { <div class="flex flex-col gap-4">
const modal = document.querySelector(
`#${modalID}`,
) as HTMLDialogElement | null
modal?.close()
}}
icon={<IconX size={20} />}
/>
</div>
<div> <div>
<ConfigTitle withDivider>{t('autoCloseConns')}</ConfigTitle> <ConfigTitle withDivider>{t('autoCloseConns')}</ConfigTitle>
@ -126,10 +117,6 @@ export const ProxiesSettingsModal = () => {
</select> </select>
</div> </div>
</div> </div>
</Modal>
<form method="dialog" class="modal-backdrop">
<button />
</form>
</dialog>
) )
} }

View File

@ -7,6 +7,7 @@ export * from './Header'
export * from './Latency' export * from './Latency'
export * from './LogoText' export * from './LogoText'
export * from './LogsSettingsModal' export * from './LogsSettingsModal'
export * from './Modal'
export * from './ProxiesSettingsModal' export * from './ProxiesSettingsModal'
export * from './ProxyCardGroups' export * from './ProxyCardGroups'
export * from './ProxyNodeCard' export * from './ProxyNodeCard'

View File

@ -163,11 +163,3 @@ export enum LOG_LEVEL {
export const LOGS_TABLE_MAX_ROWS_LIST = [200, 300, 500, 800, 1000] export const LOGS_TABLE_MAX_ROWS_LIST = [200, 300, 500, 800, 1000]
export const DEFAULT_LOGS_TABLE_MAX_ROWS = LOGS_TABLE_MAX_ROWS_LIST[0] export const DEFAULT_LOGS_TABLE_MAX_ROWS = LOGS_TABLE_MAX_ROWS_LIST[0]
export enum MODAL {
PROXIES_SETTINGS = 'proxies-settings',
RULES_SETTINGS = 'rules-settings',
CONNECTIONS_SETTINGS = 'connections-settings',
CONNECTIONS_TABLE_DETAILS = 'connections-table-details',
LOGS_SETTINGS = 'logs-settings',
}

View File

@ -2,9 +2,13 @@ export default {
add: 'Add', add: 'Add',
overview: 'Overview', overview: 'Overview',
proxies: 'Proxies', proxies: 'Proxies',
proxiesSettings: 'Proxies Settings',
rules: 'Rules', rules: 'Rules',
connections: 'Connections', connections: 'Connections',
connectionsSettings: 'Connections Settings',
connectionsDetails: 'Connections Details',
logs: 'Logs', logs: 'Logs',
logsSettings: 'Logs Settings',
config: 'Config', config: 'Config',
upload: 'Upload', upload: 'Upload',
download: 'Download', download: 'Download',

View File

@ -4,9 +4,13 @@ export default {
add: '添加', add: '添加',
overview: '概览', overview: '概览',
proxies: '代理', proxies: '代理',
proxiesSettings: '代理设置',
rules: '规则', rules: '规则',
connections: '连接', connections: '连接',
connectionsSettings: '连接设置',
connectionsDetails: '连接详情',
logs: '日志', logs: '日志',
logsSettings: '日志设置',
config: '配置', config: '配置',
upload: '上传', upload: '上传',
download: '下载', download: '下载',

View File

@ -43,7 +43,7 @@ import {
ConnectionsSettingsModal, ConnectionsSettingsModal,
ConnectionsTableDetailsModal, ConnectionsTableDetailsModal,
} from '~/components' } from '~/components'
import { CONNECTIONS_TABLE_ACCESSOR_KEY, MODAL } from '~/constants' import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants'
import { useI18n } from '~/i18n' import { useI18n } from '~/i18n'
import { import {
allConnections, allConnections,
@ -78,6 +78,9 @@ const fuzzyFilter: FilterFn<Connection> = (row, columnId, value, addMeta) => {
} }
export default () => { export default () => {
let connectionsSettingsModalRef: HTMLDialogElement | undefined
let connectionsDetailsModalRef: HTMLDialogElement | undefined
const [t] = useI18n() const [t] = useI18n()
const [activeTab, setActiveTab] = createSignal(ActiveTab.activeConnections) const [activeTab, setActiveTab] = createSignal(ActiveTab.activeConnections)
@ -103,11 +106,7 @@ export default () => {
onClick={() => { onClick={() => {
setSelectedConnectionID(row.original.id) setSelectedConnectionID(row.original.id)
const modal = document.querySelector( connectionsDetailsModalRef?.showModal()
`#${MODAL.CONNECTIONS_TABLE_DETAILS}`,
) as HTMLDialogElement | null
modal?.showModal()
}} }}
icon={<IconInfoSmall size="16" />} icon={<IconInfoSmall size="16" />}
/> />
@ -397,13 +396,7 @@ export default () => {
<Button <Button
class="btn join-item btn-sm sm:btn-md" class="btn join-item btn-sm sm:btn-md"
onClick={() => { onClick={() => connectionsSettingsModalRef?.showModal()}
const modal = document.querySelector(
`#${MODAL.CONNECTIONS_SETTINGS}`,
) as HTMLDialogElement | null
modal?.showModal()
}}
icon={<IconSettings />} icon={<IconSettings />}
/> />
</div> </div>
@ -531,6 +524,7 @@ export default () => {
</div> </div>
<ConnectionsSettingsModal <ConnectionsSettingsModal
ref={(el) => (connectionsSettingsModalRef = el)}
order={connectionsTableColumnOrder()} order={connectionsTableColumnOrder()}
visible={connectionsTableColumnVisibility()} visible={connectionsTableColumnVisibility()}
onOrderChange={(data) => setConnectionsTableColumnOrder(data)} onOrderChange={(data) => setConnectionsTableColumnOrder(data)}
@ -540,6 +534,7 @@ export default () => {
/> />
<ConnectionsTableDetailsModal <ConnectionsTableDetailsModal
ref={(el) => (connectionsDetailsModalRef = el)}
selectedConnectionID={selectedConnectionID()} selectedConnectionID={selectedConnectionID()}
/> />
</div> </div>

View File

@ -18,7 +18,7 @@ import {
import { For, Index, createEffect, createSignal } from 'solid-js' import { For, Index, createEffect, createSignal } from 'solid-js'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { Button, LogsSettingsModal } from '~/components' import { Button, LogsSettingsModal } from '~/components'
import { LOG_LEVEL, MODAL } from '~/constants' import { LOG_LEVEL } from '~/constants'
import { useI18n } from '~/i18n' import { useI18n } from '~/i18n'
import { logsTableSize, tableSizeClassName, useWsRequest } from '~/signals' import { logsTableSize, tableSizeClassName, useWsRequest } from '~/signals'
import { logLevel, logMaxRows } from '~/signals/config' import { logLevel, logMaxRows } from '~/signals/config'
@ -40,7 +40,10 @@ const fuzzyFilter: FilterFn<LogWithSeq> = (row, columnId, value, addMeta) => {
} }
export default () => { export default () => {
let logsSettingsModalRef: HTMLDialogElement | undefined
const [t] = useI18n() const [t] = useI18n()
let seq = 1 let seq = 1
const [logs, setLogs] = createSignal<LogWithSeq[]>([]) const [logs, setLogs] = createSignal<LogWithSeq[]>([])
@ -139,13 +142,7 @@ export default () => {
<Button <Button
class="join-item btn-sm sm:btn-md" class="join-item btn-sm sm:btn-md"
onClick={() => { onClick={() => logsSettingsModalRef?.showModal()}
const modal = document.querySelector(
`#${MODAL.LOGS_SETTINGS}`,
) as HTMLDialogElement | null
modal?.showModal()
}}
icon={<IconSettings />} icon={<IconSettings />}
/> />
</div> </div>
@ -221,7 +218,7 @@ export default () => {
</table> </table>
</div> </div>
<LogsSettingsModal /> <LogsSettingsModal ref={(el) => (logsSettingsModalRef = el)} />
</div> </div>
) )
} }

View File

@ -14,7 +14,6 @@ import {
RenderInTwoColumns, RenderInTwoColumns,
SubscriptionInfo, SubscriptionInfo,
} from '~/components' } from '~/components'
import { MODAL } from '~/constants'
import { import {
filterProxiesByAvailability, filterProxiesByAvailability,
sortProxiesByOrderingType, sortProxiesByOrderingType,
@ -34,7 +33,10 @@ enum ActiveTab {
} }
export default () => { export default () => {
let proxiesSettingsModalRef: HTMLDialogElement | undefined
const [t] = useI18n() const [t] = useI18n()
const { const {
proxies, proxies,
selectProxyInGroup, selectProxyInGroup,
@ -133,13 +135,7 @@ export default () => {
<div class="ml-auto"> <div class="ml-auto">
<Button <Button
class="btn-circle btn-sm sm:btn-md" class="btn-circle btn-sm sm:btn-md"
onClick={() => { onClick={() => proxiesSettingsModalRef?.showModal()}
const modal = document.querySelector(
`#${MODAL.PROXIES_SETTINGS}`,
) as HTMLDialogElement | null
modal?.showModal()
}}
icon={<IconSettings />} icon={<IconSettings />}
/> />
</div> </div>
@ -310,7 +306,7 @@ export default () => {
</Show> </Show>
</div> </div>
<ProxiesSettingsModal /> <ProxiesSettingsModal ref={(el) => (proxiesSettingsModalRef = el)} />
</div> </div>
) )
} }