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" />
</Show>
<span class="truncate" classList={{ 'flex-1': !local.icon }}>
<span
class="truncate rounded-none"
classList={{
'flex-1': !local.icon,
}}
>
{props.icon || props.children}
</span>
</button>

View File

@ -28,7 +28,7 @@ export const Collapse: ParentComponent<Props> = (props) => {
<div
class={twMerge(
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

View File

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

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

View File

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

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

View File

@ -7,6 +7,7 @@ export * from './Header'
export * from './Latency'
export * from './LogoText'
export * from './LogsSettingsModal'
export * from './Modal'
export * from './ProxiesSettingsModal'
export * from './ProxyCardGroups'
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 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',
overview: 'Overview',
proxies: 'Proxies',
proxiesSettings: 'Proxies Settings',
rules: 'Rules',
connections: 'Connections',
connectionsSettings: 'Connections Settings',
connectionsDetails: 'Connections Details',
logs: 'Logs',
logsSettings: 'Logs Settings',
config: 'Config',
upload: 'Upload',
download: 'Download',

View File

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

View File

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

View File

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

View File

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