diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index 32e1ed8..f5b19d3 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -1,115 +1,86 @@ import classnames from 'classnames' -import { useRef, useLayoutEffect, useState, useMemo, ReactElement, Children, cloneElement } from 'react' +import { useRef, useState, useMemo, useLayoutEffect } from 'react' import { createPortal } from 'react-dom' import { Icon } from '@components' import { noop } from '@lib/helper' import { BaseComponentProps } from '@models' + import './style.scss' -type OptionValue = string | number +export interface SelectOptions { + label: string + value: T + disabled?: boolean + key?: React.Key +} -interface SelectProps extends BaseComponentProps { +interface SelectProps extends BaseComponentProps { /** * selected value * must match one of options */ - value: OptionValue + value: T - children?: ReactElement + options: Array> - onSelect?: (value: OptionValue, e: React.MouseEvent) => void + onSelect?: (value: T, e: React.MouseEvent) => void } -export function Select (props: SelectProps) { - const { value, onSelect, children, className: cn, style } = props +export function Select (props: SelectProps) { + const { value, options, onSelect, className: cn, style } = props - const portalRef = useRef(document.createElement('div')) - const attachmentRef = useRef(null) + const portalRef = useRef(document.createElement('div')) const targetRef = useRef(null) const [showDropDownList, setShowDropDownList] = useState(false) - const [hasCreateDropList, setHasCreateDropList] = useState(false) - const dropdownListStyles = useMemo(() => { - if (targetRef.current != null) { - const targetRectInfo = targetRef.current.getBoundingClientRect() - return { - top: Math.floor(targetRectInfo.top) - 10, - left: Math.floor(targetRectInfo.left) - 10, - } - } - return {} + const [dropdownListStyles, setDropdownListStyles] = useState({}) + useLayoutEffect(() => { + const targetRectInfo = targetRef.current!.getBoundingClientRect() + setDropdownListStyles({ + top: Math.floor(targetRectInfo.top + targetRectInfo.height) + 6, + left: Math.floor(targetRectInfo.left) - 10, + }) }, []) - function handleGlobalClick (e: MouseEvent) { - const el = attachmentRef.current - - if (el?.contains(e.target as Node)) { - setShowDropDownList(false) - } - } - useLayoutEffect(() => { const current = portalRef.current document.body.appendChild(current) - document.addEventListener('click', handleGlobalClick, true) return () => { - document.addEventListener('click', handleGlobalClick, true) document.body.removeChild(current) } }, []) function handleShowDropList () { - if (!hasCreateDropList) { - setHasCreateDropList(true) - } - setShowDropDownList(true) + setShowDropDownList(!showDropDownList) } - const matchChild = useMemo(() => { - let matchChild: React.ReactElement | null = null - - Children.forEach(children, (child) => { - if (child?.props?.value === value) { - matchChild = child - } - }) - - return matchChild as React.ReactElement | null - }, [value, children]) - - const hookedChildren = useMemo(() => { - return Children.map(children ?? [], child => { - if (!child.props || !child.type) { - return child - } - - // add classname for selected option - const className = child.props.value === value - ? classnames(child.props.className, 'selected') - : child.props.className - - // hook element onclick event - const rawOnClickEvent = child.props.onClick - return cloneElement(child, Object.assign({}, child.props, { - onClick: (e: React.MouseEvent) => { - onSelect?.(child.props.value, e) - setShowDropDownList(false) - rawOnClickEvent?.(e) - }, - className, - })) - }) - }, [children, value, onSelect]) + const matchChild = useMemo( + () => options.find(o => o.value === value), + [value, options], + ) const dropDownList = (
    - { hookedChildren } + { + options.map(option => ( + + )) + }
) @@ -122,28 +93,25 @@ export function Select (props: SelectProps) { ref={targetRef} onClick={handleShowDropList} > - {matchChild?.props?.children} + {matchChild?.label} - { - hasCreateDropList && createPortal(dropDownList, portalRef.current) - } + {createPortal(dropDownList, portalRef.current)} ) } -interface OptionProps extends BaseComponentProps { - key: React.Key - value: OptionValue +interface OptionProps extends BaseComponentProps { + value: T disabled?: boolean onClick?: (e: React.MouseEvent) => void } -export function Option (props: OptionProps) { - const { className: cn, style, key, disabled = false, children, onClick = noop } = props +function Option (props: OptionProps) { + const { className: cn, style, disabled = false, children, onClick = noop } = props const className = classnames('option', { disabled }, cn) return ( -
  • {children}
  • +
  • {children}
  • ) } diff --git a/src/components/Select/style.scss b/src/components/Select/style.scss index a811b2e..ba54c45 100644 --- a/src/components/Select/style.scss +++ b/src/components/Select/style.scss @@ -16,22 +16,16 @@ position: absolute; max-width: 170px; border-radius: 4px; - overflow: hidden; - box-shadow: 0 2px 5px rgba($color: $color-gray-dark, $alpha: 0.5); - opacity: 0.8; pointer-events: none; - transform: scaleY(0); - transform-origin: top; - transition: all 200ms linear; + transition: all 200ms ease; .list { + opacity: 0; max-height: 300px; overflow: auto; background: $color-white; padding: 5px 0; - transform: scaleY(2); - transform-origin: top; - transition: all 200ms linear; + transition: all 200ms ease; > .option { color: $color-primary-darken; @@ -52,11 +46,12 @@ } .select-list-show { - opacity: 1; pointer-events: visible; transform: scaleY(1); + box-shadow: 0 2px 5px rgba($color: $color-gray-dark, $alpha: 0.5); .list { + opacity: 1; transform: scaleY(1); } } diff --git a/src/containers/Connections/index.tsx b/src/containers/Connections/index.tsx index 2f148ee..534f756 100644 --- a/src/containers/Connections/index.tsx +++ b/src/containers/Connections/index.tsx @@ -1,4 +1,4 @@ -import { useIntersectionObserver, useSyncedRef } from '@react-hookz/web/esm' +import { useIntersectionObserver, useSyncedRef, useUnmountEffect } from '@react-hookz/web/esm' import { useTableInstance, createTable, getSortedRowModelSync, getColumnFilteredRowModelSync, getCoreRowModelSync } from '@tanstack/react-table' import classnames from 'classnames' import produce from 'immer' @@ -53,6 +53,7 @@ export default function Connections () { const { translation, lang } = useI18n() const t = useMemo(() => translation('Connections').t, [translation]) const connStreamReader = useConnectionStreamReader() + const readerRef = useSyncedRef(connStreamReader) const client = useClient() const cardRef = useRef(null) @@ -156,9 +157,11 @@ export default function Connections () { connStreamReader?.subscribe('data', handleConnection) return () => { connStreamReader?.unsubscribe('data', handleConnection) - connStreamReader?.destory() } }, [connStreamReader, feed, setTraffic]) + useUnmountEffect(() => { + readerRef.current?.destory() + }) const instance = useTableInstance(table, { data, diff --git a/src/containers/Logs/index.tsx b/src/containers/Logs/index.tsx index 8fe16ff..b09f39d 100644 --- a/src/containers/Logs/index.tsx +++ b/src/containers/Logs/index.tsx @@ -1,17 +1,34 @@ import dayjs from 'dayjs' +import { camelCase } from 'lodash-es' import { useLayoutEffect, useEffect, useRef, useState } from 'react' -import { Card, Header } from '@components' +import { Select, Card, Header } from '@components' import { Log } from '@models/Log' -import { useI18n, useLogsStreamReader } from '@stores' +import { useConfig, useI18n, useLogsStreamReader } from '@stores' import './style.scss' +const logLevelOptions = [ + { label: 'Default', value: '' }, + { label: 'Debug', value: 'debug' }, + { label: 'Info', value: 'info' }, + { label: 'Warn', value: 'warning' }, + { label: 'Error', value: 'error' }, + { label: 'Silent', value: 'silent' }, +] +const logMap = new Map([ + ['debug', 'text-teal-500'], + ['info', 'text-sky-500'], + ['warning', 'text-pink-500'], + ['error', 'text-rose-500'], +]) + export default function Logs () { const listRef = useRef(null) const logsRef = useRef([]) const [logs, setLogs] = useState([]) const { translation } = useI18n() + const { data: { logLevel }, set: setConfig } = useConfig() const { t } = translation('Logs') const logsStreamReader = useLogsStreamReader() const scrollHeightRef = useRef(listRef.current?.scrollHeight ?? 0) @@ -35,21 +52,29 @@ export default function Logs () { logsRef.current = logsStreamReader.buffer() setLogs(logsRef.current) } - return () => logsStreamReader?.unsubscribe('data', handleLog) }, [logsStreamReader]) return (
    -
    +
    + {t('levelLabel')}: +