mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 12:22:07 +08:00
Route frontend and tray node selections through the same backend flow for selection, persistence, state refresh, and connection cleanup. Read the group's previous selection before switching and preserve the original rule of closing only connections whose chains contain that selection. Run optional cleanup in the background and keep its failures separate from successful selection. Remove redundant frontend effects and unused IPC commands, and refresh the proxy view for every successful switch. Refs:32095daf90Refs:bea0dde074
679 lines
19 KiB
TypeScript
679 lines
19 KiB
TypeScript
import { DragDropProvider } from '@dnd-kit/react'
|
|
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
|
|
import { useLockFn } from 'ahooks'
|
|
import { throttle } from 'lodash-es'
|
|
import {
|
|
lazy,
|
|
Suspense,
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
|
|
import {
|
|
BaseEmpty,
|
|
BaseLoading,
|
|
StickyVirtualList,
|
|
type StickyVirtualListHandle,
|
|
} from '@/components/base'
|
|
import { useProfiles } from '@/hooks/use-profiles'
|
|
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
|
import { useVerge } from '@/hooks/use-verge'
|
|
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
|
import delayManager from '@/services/delay'
|
|
import {
|
|
isInteractableMember,
|
|
resolveMember,
|
|
type ProxyGroupView,
|
|
type ResolvedProxyMember,
|
|
} from '@/types/proxy-view'
|
|
import { debugLog } from '@/utils/debug'
|
|
|
|
import type { ProxyChainItem } from './proxy-chain-model'
|
|
import { ProxyEmptyState } from './proxy-empty-state'
|
|
import {
|
|
resolveEmptyListReason,
|
|
resolveProxyListState,
|
|
} from './proxy-empty-state-model'
|
|
import {
|
|
DEFAULT_HOVER_DELAY,
|
|
ProxyGroupNavigator,
|
|
} from './proxy-group-navigator'
|
|
import { ProxyRender } from './proxy-render'
|
|
import type { HeadState } from './use-head-state'
|
|
import {
|
|
PROXY_GROUP_HEADER_SENSORS,
|
|
useProxyGroupHeaderLayout,
|
|
} from './use-proxy-group-header-layout'
|
|
import {
|
|
hasRenderableItems,
|
|
type IRenderItem,
|
|
useRenderList,
|
|
} from './use-render-list'
|
|
|
|
const ProxyGroupsChain = lazy(() =>
|
|
import('./proxy-groups-chain').then((m) => ({
|
|
default: m.ProxyGroupsChain,
|
|
})),
|
|
)
|
|
|
|
function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
|
|
const ref = useRef(fn)
|
|
ref.current = fn
|
|
return useCallback((...args: Parameters<T>) => ref.current(...args), []) as T
|
|
}
|
|
|
|
interface Props {
|
|
mode: string
|
|
isChainMode?: boolean
|
|
chainConfigItems?: ProxyChainItem[] | null
|
|
onExitChainMode: () => void
|
|
}
|
|
|
|
function useEmptyRenderList() {
|
|
const { isProxyViewError } = useProxiesData()
|
|
const { runningMode } = useSystemData()
|
|
|
|
return (
|
|
<ProxyEmptyState
|
|
reason={resolveEmptyListReason({ runningMode, isProxyViewError })}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function useProxyRenderState(
|
|
mode: string,
|
|
isChainMode: boolean,
|
|
activeSelectedGroup: string | null,
|
|
) {
|
|
const { verge } = useVerge()
|
|
const { proxyView } = useProxiesData()
|
|
const {
|
|
renderList,
|
|
onProxies,
|
|
onHeadState: setListHeadState,
|
|
} = useRenderList(mode, isChainMode, activeSelectedGroup)
|
|
const scrollPositionKey = useMemo(
|
|
() =>
|
|
isChainMode
|
|
? `${mode}:chain:${activeSelectedGroup ?? 'all'}`
|
|
: `${mode}:normal`,
|
|
[activeSelectedGroup, isChainMode, mode],
|
|
)
|
|
|
|
const onHeadState = useCallback(
|
|
(groupName: string, patch: Partial<HeadState>) => {
|
|
if (patch.open) onProxies()
|
|
setListHeadState(groupName, patch)
|
|
},
|
|
[onProxies, setListHeadState],
|
|
)
|
|
|
|
const timeout = verge?.default_latency_timeout || 10000
|
|
|
|
const handleCheckAll = useStableCallback(
|
|
useLockFn(async (groupName: string) => {
|
|
debugLog(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`)
|
|
|
|
const group =
|
|
proxyView?.groups.find(({ name }) => name === groupName) ??
|
|
(proxyView?.global?.name === groupName ? proxyView.global : undefined)
|
|
const occurrences =
|
|
proxyView && group
|
|
? group.members.map((member, memberIndex) => ({
|
|
memberIndex,
|
|
member: resolveMember(proxyView, member),
|
|
}))
|
|
: []
|
|
const interactable = occurrences
|
|
.map(({ member }) => member)
|
|
.filter(isInteractableMember)
|
|
|
|
debugLog(`[ProxyGroups] 找到代理数量: ${interactable.length}`)
|
|
|
|
const url = delayManager.getUrl(groupName)
|
|
debugLog(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`)
|
|
|
|
try {
|
|
await delayManager.checkListDelay(interactable, groupName, timeout)
|
|
debugLog(`[ProxyGroups] 延迟测试完成,组: ${groupName}`)
|
|
} catch (error) {
|
|
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
|
} finally {
|
|
onProxies()
|
|
}
|
|
}),
|
|
)
|
|
|
|
const saveScrollPosition = useCallback(
|
|
(scrollTop: number) => {
|
|
const scrollPositions = localStorage.getItem('proxy-scroll-positions')
|
|
? JSON.parse(localStorage.getItem('proxy-scroll-positions') ?? '{}')
|
|
: {}
|
|
scrollPositions[scrollPositionKey] = scrollTop
|
|
try {
|
|
localStorage.setItem(
|
|
'proxy-scroll-positions',
|
|
JSON.stringify(scrollPositions),
|
|
)
|
|
} catch (e) {
|
|
console.error('Error saving scroll position:', e)
|
|
}
|
|
},
|
|
[scrollPositionKey],
|
|
)
|
|
|
|
const getScrollPosition = useCallback(() => {
|
|
try {
|
|
const savedPositions = localStorage.getItem('proxy-scroll-positions')
|
|
if (savedPositions) {
|
|
const positions = JSON.parse(savedPositions)
|
|
const savedPosition = positions[scrollPositionKey]
|
|
return savedPosition ?? 0
|
|
}
|
|
} catch (e) {
|
|
console.error('Error restoring scroll position:', e)
|
|
}
|
|
}, [scrollPositionKey])
|
|
|
|
return {
|
|
verge,
|
|
renderList,
|
|
onProxies,
|
|
onHeadState,
|
|
setHeadState: setListHeadState,
|
|
handleCheckAll,
|
|
saveScrollPosition,
|
|
getScrollPosition,
|
|
}
|
|
}
|
|
|
|
function ChainProxyGroups(props: {
|
|
mode: string
|
|
chainConfigItems?: ProxyChainItem[] | null
|
|
onExitChainMode: () => void
|
|
}) {
|
|
const { mode, chainConfigItems, onExitChainMode } = props
|
|
const { proxyView } = useProxiesData()
|
|
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
|
|
|
const availableGroups = useMemo(() => {
|
|
const groups = proxyView?.groups
|
|
if (!groups) return []
|
|
return groups.filter(
|
|
(group) => group.type === 'Selector' || group.type === 'URLTest',
|
|
)
|
|
}, [proxyView?.groups])
|
|
|
|
const defaultRuleGroup = useMemo(() => {
|
|
if (mode === 'rule' && availableGroups.length > 0) {
|
|
return availableGroups[0].name
|
|
}
|
|
return null
|
|
}, [availableGroups, mode])
|
|
|
|
const activeSelectedGroup = selectedGroup ?? defaultRuleGroup
|
|
const {
|
|
renderList,
|
|
onHeadState,
|
|
handleCheckAll,
|
|
getScrollPosition,
|
|
saveScrollPosition,
|
|
} = useProxyRenderState(mode, true, activeSelectedGroup)
|
|
const emptyList = useEmptyRenderList()
|
|
|
|
const parentRef = useRef<HTMLDivElement>(null)
|
|
const scrollTopRef = useRef(0)
|
|
const showScrollTopRef = useRef(false)
|
|
const activeStickyIndexRef = useRef<number | null>(null)
|
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
|
const stickyGroupIndexes = useMemo(
|
|
() =>
|
|
renderList.flatMap((item, index) =>
|
|
item.type === 0 && !item.group.hidden ? [index] : [],
|
|
),
|
|
[renderList],
|
|
)
|
|
|
|
const rangeExtractor = useCallback(
|
|
(range: Parameters<typeof defaultRangeExtractor>[0]) => {
|
|
const activeStickyIndex = [...stickyGroupIndexes]
|
|
.reverse()
|
|
.find((index) => index <= range.startIndex)
|
|
activeStickyIndexRef.current = activeStickyIndex ?? null
|
|
|
|
const indexes = defaultRangeExtractor(range)
|
|
return activeStickyIndex == null || indexes.includes(activeStickyIndex)
|
|
? indexes
|
|
: [activeStickyIndex, ...indexes]
|
|
},
|
|
[stickyGroupIndexes],
|
|
)
|
|
|
|
const virtualizer = useVirtualizer({
|
|
count: renderList.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 56,
|
|
overscan: 15,
|
|
getItemKey: (index) => renderList[index]?.key ?? index,
|
|
rangeExtractor,
|
|
})
|
|
const virtualItems = virtualizer.getVirtualItems()
|
|
const activeStickyIndex = activeStickyIndexRef.current
|
|
|
|
useLayoutEffect(() => {
|
|
if (renderList.length === 0) return
|
|
const node = parentRef.current
|
|
if (!node) return
|
|
|
|
const savedPosition = getScrollPosition()
|
|
if (savedPosition !== undefined) {
|
|
node.scrollTop = savedPosition
|
|
scrollTopRef.current = savedPosition
|
|
const nextShowScrollTop = savedPosition > 100
|
|
showScrollTopRef.current = nextShowScrollTop
|
|
queueMicrotask(() => setShowScrollTop(nextShowScrollTop))
|
|
}
|
|
}, [renderList.length, getScrollPosition])
|
|
|
|
const saveScrollPositionThrottled = useMemo(
|
|
() => throttle(saveScrollPosition, 500),
|
|
[saveScrollPosition],
|
|
)
|
|
|
|
const handleScroll = useCallback(
|
|
(event: Event) => {
|
|
const target = event.target as HTMLElement | null
|
|
const nextScrollTop = target?.scrollTop ?? 0
|
|
const nextShowScrollTop = nextScrollTop > 100
|
|
scrollTopRef.current = nextScrollTop
|
|
|
|
if (showScrollTopRef.current !== nextShowScrollTop) {
|
|
showScrollTopRef.current = nextShowScrollTop
|
|
setShowScrollTop(nextShowScrollTop)
|
|
}
|
|
|
|
saveScrollPositionThrottled(nextScrollTop)
|
|
},
|
|
[saveScrollPositionThrottled],
|
|
)
|
|
|
|
useEffect(() => {
|
|
const node = parentRef.current
|
|
if (!node) return
|
|
|
|
const listener = handleScroll as EventListener
|
|
const options: AddEventListenerOptions = { passive: true }
|
|
|
|
node.addEventListener('scroll', listener, options)
|
|
|
|
return () => {
|
|
saveScrollPosition(scrollTopRef.current)
|
|
node.removeEventListener('scroll', listener, options)
|
|
}
|
|
}, [handleScroll, saveScrollPosition])
|
|
|
|
const scrollToTop = useCallback(() => {
|
|
parentRef.current?.scrollTo?.({
|
|
top: 0,
|
|
behavior: 'smooth',
|
|
})
|
|
scrollTopRef.current = 0
|
|
}, [])
|
|
|
|
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
|
if (!group) return
|
|
const { name, now } = group
|
|
|
|
const index = renderList.findIndex(
|
|
(item) =>
|
|
item.group?.name === name &&
|
|
((item.type === 2 && item.member?.member.ref.name === now) ||
|
|
(item.type === 4 &&
|
|
item.memberCol?.some(({ member }) => member.ref.name === now))),
|
|
)
|
|
|
|
if (index >= 0) {
|
|
virtualizer.scrollToIndex(index, {
|
|
align: 'center',
|
|
behavior: 'smooth',
|
|
})
|
|
}
|
|
})
|
|
|
|
if (!hasRenderableItems(renderList)) return emptyList
|
|
|
|
return (
|
|
<Suspense fallback={<BaseLoading />}>
|
|
<ProxyGroupsChain
|
|
mode={mode}
|
|
chainConfigItems={chainConfigItems}
|
|
onExitChainMode={onExitChainMode}
|
|
availableGroups={availableGroups}
|
|
activeSelectedGroup={activeSelectedGroup}
|
|
showScrollTop={showScrollTop}
|
|
parentRef={parentRef}
|
|
totalSize={virtualizer.getTotalSize()}
|
|
virtualItems={virtualItems}
|
|
renderList={renderList}
|
|
activeStickyIndex={activeStickyIndex}
|
|
measureElement={virtualizer.measureElement}
|
|
onCheckAll={handleCheckAll}
|
|
onHeadState={onHeadState}
|
|
onLocation={handleLocation}
|
|
onGroupSelect={setSelectedGroup}
|
|
onScrollToTop={scrollToTop}
|
|
/>
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
function NormalProxyGroups(props: { mode: string }) {
|
|
const { mode } = props
|
|
const stickyListRef = useRef<StickyVirtualListHandle>(null)
|
|
const {
|
|
verge,
|
|
renderList,
|
|
onProxies,
|
|
onHeadState,
|
|
setHeadState,
|
|
handleCheckAll,
|
|
getScrollPosition,
|
|
saveScrollPosition,
|
|
} = useProxyRenderState(mode, false, null)
|
|
const emptyList = useEmptyRenderList()
|
|
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout()
|
|
const { proxyView } = useProxiesData()
|
|
const globalGroupName = proxyView?.global?.name
|
|
// The revealed group sits below GLOBAL's rows, so a click must bring it into view.
|
|
const pendingRevealRef = useRef<string | null>(null)
|
|
const renderFirstRef = useRef(true)
|
|
// Do not persist intermediate positions produced while restoring virtual scroll.
|
|
const isRestoringRef = useRef(false)
|
|
|
|
useLayoutEffect(() => {
|
|
if (renderList.length === 0) return
|
|
if (!renderFirstRef.current) return
|
|
const node = stickyListRef.current?.getScrollElement()
|
|
if (!node) return
|
|
|
|
const savedPosition = getScrollPosition()
|
|
if (!savedPosition) {
|
|
renderFirstRef.current = false
|
|
return
|
|
}
|
|
|
|
// Retry across frames until virtual-list measurements can reach the saved offset.
|
|
isRestoringRef.current = true
|
|
let rafId = 0
|
|
let attempts = 0
|
|
const maxAttempts = 30
|
|
|
|
const step = () => {
|
|
const el = stickyListRef.current?.getScrollElement()
|
|
if (!el) {
|
|
isRestoringRef.current = false
|
|
return
|
|
}
|
|
|
|
el.scrollTop = savedPosition
|
|
attempts += 1
|
|
|
|
const reached = Math.abs(el.scrollTop - savedPosition) <= 1
|
|
if (reached || attempts >= maxAttempts) {
|
|
renderFirstRef.current = false
|
|
isRestoringRef.current = false
|
|
return
|
|
}
|
|
|
|
rafId = requestAnimationFrame(step)
|
|
}
|
|
|
|
rafId = requestAnimationFrame(step)
|
|
return () => {
|
|
cancelAnimationFrame(rafId)
|
|
isRestoringRef.current = false
|
|
}
|
|
}, [renderList.length, getScrollPosition])
|
|
|
|
const saveScrollPositionThrottled = useMemo(
|
|
() => throttle(saveScrollPosition, 500),
|
|
[saveScrollPosition],
|
|
)
|
|
|
|
const handleScroll = useCallback(
|
|
(event: Event) => {
|
|
if (isRestoringRef.current) return
|
|
const target = event.target as HTMLElement | null
|
|
const nextScrollTop = target?.scrollTop ?? 0
|
|
|
|
saveScrollPositionThrottled(nextScrollTop)
|
|
},
|
|
[saveScrollPositionThrottled],
|
|
)
|
|
|
|
useEffect(() => {
|
|
const node = stickyListRef.current?.getScrollElement()
|
|
if (!node) return
|
|
|
|
const listener = handleScroll as EventListener
|
|
const options: AddEventListenerOptions = { passive: true }
|
|
|
|
node.addEventListener('scroll', listener, options)
|
|
|
|
return () => {
|
|
node.removeEventListener('scroll', listener, options)
|
|
}
|
|
}, [handleScroll])
|
|
|
|
const { select } = useProxySelection({
|
|
onError: (error) => {
|
|
console.error('代理切换失败', error)
|
|
onProxies()
|
|
},
|
|
})
|
|
|
|
const handleChangeProxy = useCallback(
|
|
(group: ProxyGroupView, member: ResolvedProxyMember) => {
|
|
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
|
if (!isInteractableMember(member)) return
|
|
|
|
if (group.name === globalGroupName) {
|
|
pendingRevealRef.current =
|
|
member.kind === 'group' ? member.ref.name : null
|
|
// The backend selection event refreshes the view.
|
|
if (member.kind === 'group') {
|
|
setHeadState(member.ref.name, { open: true })
|
|
}
|
|
}
|
|
select(group.name, member.ref.name, group.fixed!)
|
|
},
|
|
[globalGroupName, select, setHeadState],
|
|
)
|
|
|
|
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
|
if (!group) return
|
|
const { name, now } = group
|
|
|
|
const index = renderList.findIndex(
|
|
(e) =>
|
|
e.group?.name === name &&
|
|
((e.type === 2 && e.member?.member.ref.name === now) ||
|
|
(e.type === 4 &&
|
|
e.memberCol?.some(({ member }) => member.ref.name === now))),
|
|
)
|
|
|
|
if (index >= 0) {
|
|
stickyListRef.current?.scrollToIndex(index, {
|
|
align: 'center',
|
|
behavior: 'smooth',
|
|
})
|
|
}
|
|
})
|
|
|
|
const handleGroupLocationByName = useCallback(
|
|
(groupName: string) => {
|
|
const index = renderList.findIndex(
|
|
(item) => item.type === 0 && item.group?.name === groupName,
|
|
)
|
|
|
|
if (index >= 0) {
|
|
stickyListRef.current?.scrollToIndex(index, {
|
|
align: 'start',
|
|
behavior: 'smooth',
|
|
})
|
|
}
|
|
},
|
|
[renderList],
|
|
)
|
|
|
|
const proxyGroupNames = useMemo(() => {
|
|
const names = renderList
|
|
.filter((item) => item.type === 0 && item.group?.name)
|
|
.map((item) => item.group!.name)
|
|
return Array.from(new Set(names))
|
|
}, [renderList])
|
|
|
|
useEffect(() => {
|
|
if (mode !== 'global') {
|
|
pendingRevealRef.current = null
|
|
return
|
|
}
|
|
const groupName = pendingRevealRef.current
|
|
if (!groupName || !proxyGroupNames.includes(groupName)) return
|
|
pendingRevealRef.current = null
|
|
handleGroupLocationByName(groupName)
|
|
}, [handleGroupLocationByName, mode, proxyGroupNames])
|
|
|
|
const handleGroupToggle = useCallback(
|
|
async (group: ProxyGroupView) => {
|
|
const index = renderList.findIndex(
|
|
(item) => item.type === 0 && item.group.name === group.name,
|
|
)
|
|
if (index < 0) return
|
|
|
|
if (!stickyListRef.current?.isItemScrolledPastStart(index, 1)) return
|
|
|
|
stickyListRef.current.scrollToIndex(index, {
|
|
align: 'start',
|
|
behavior: 'auto',
|
|
})
|
|
|
|
await new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => resolve())
|
|
})
|
|
},
|
|
[renderList],
|
|
)
|
|
|
|
const renderGroupItem = useCallback(
|
|
(item: IRenderItem, _index: number, stickyed: boolean) => (
|
|
<ProxyRender
|
|
item={item}
|
|
stickyed={stickyed}
|
|
onLocation={handleLocation}
|
|
onCheckAll={handleCheckAll}
|
|
onHeadState={async (groupName, patch) => {
|
|
if (stickyed && patch.filterText !== undefined) {
|
|
handleGroupLocationByName(groupName)
|
|
await stickyListRef.current?.waitForScrollEnd()
|
|
}
|
|
onHeadState(groupName, patch)
|
|
}}
|
|
onChangeProxy={handleChangeProxy}
|
|
onGroupToggle={handleGroupToggle}
|
|
/>
|
|
),
|
|
[
|
|
handleChangeProxy,
|
|
handleCheckAll,
|
|
onHeadState,
|
|
handleLocation,
|
|
handleGroupToggle,
|
|
handleGroupLocationByName,
|
|
],
|
|
)
|
|
|
|
const renderProxyItem = useCallback(
|
|
(item: IRenderItem) => (
|
|
<ProxyRender
|
|
key={item.key}
|
|
item={item}
|
|
onLocation={handleLocation}
|
|
onCheckAll={handleCheckAll}
|
|
onHeadState={onHeadState}
|
|
onChangeProxy={handleChangeProxy}
|
|
/>
|
|
),
|
|
[handleChangeProxy, handleCheckAll, onHeadState, handleLocation],
|
|
)
|
|
|
|
if (!hasRenderableItems(renderList)) return emptyList
|
|
|
|
return (
|
|
<div style={{ position: 'relative', height: '100%' }}>
|
|
<DragDropProvider
|
|
sensors={PROXY_GROUP_HEADER_SENSORS}
|
|
onDragEnd={onHeaderDragEnd}
|
|
>
|
|
<StickyVirtualList
|
|
ref={stickyListRef}
|
|
items={renderList}
|
|
isGroupItem={(item) => item.type === 0}
|
|
getItemKey={(item) => item.key}
|
|
estimateGroupItemHeight={76}
|
|
estimateItemHeight={64}
|
|
renderGroupItem={renderGroupItem}
|
|
renderItem={renderProxyItem}
|
|
/>
|
|
</DragDropProvider>
|
|
|
|
{mode === 'rule' && (
|
|
<ProxyGroupNavigator
|
|
proxyGroupNames={proxyGroupNames}
|
|
onGroupLocation={handleGroupLocationByName}
|
|
enableHoverJump={verge?.enable_hover_jump_navigator ?? true}
|
|
hoverDelay={verge?.hover_jump_navigator_delay ?? DEFAULT_HOVER_DELAY}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const ProxyGroups = (props: Props) => {
|
|
const { mode, isChainMode = false, chainConfigItems, onExitChainMode } = props
|
|
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
|
const { isProxyViewPending } = useProxiesData()
|
|
const { isRunningModePending } = useSystemData()
|
|
|
|
const listState = resolveProxyListState({
|
|
mode,
|
|
profiles,
|
|
isProfilesPending: !profiles && isProfilesLoading,
|
|
isProxyViewPending,
|
|
isRunningModePending,
|
|
})
|
|
|
|
switch (listState.kind) {
|
|
case 'direct':
|
|
return <BaseEmpty textKey="proxies.page.messages.directMode" />
|
|
case 'loading':
|
|
return <BaseLoading />
|
|
case 'empty':
|
|
return <ProxyEmptyState reason={listState.reason} />
|
|
case 'render':
|
|
return isChainMode ? (
|
|
<ChainProxyGroups
|
|
mode={mode}
|
|
chainConfigItems={chainConfigItems}
|
|
onExitChainMode={onExitChainMode}
|
|
/>
|
|
) : (
|
|
<NormalProxyGroups mode={mode} />
|
|
)
|
|
}
|
|
}
|