mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-26 05:42:07 +08:00
Ports port saving, webdav/local backup operations, core change/restart/ upgrade, service install/uninstall/repair flows, the system-proxy toggle queue, the DNS override toggle, and app lifecycle commands onto the single funnel. Bespoke flows keep their own error handling via errorNotice:false; simple flows gain the funnel's default error toast and revalidation.
223 lines
6.7 KiB
TypeScript
223 lines
6.7 KiB
TypeScript
import { Alert } from '@mui/material'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { BaseDialog } from '@/components/base'
|
|
import {
|
|
continueWithSidecar,
|
|
getRuntimeState,
|
|
installService,
|
|
reinstallService,
|
|
repairService,
|
|
restartCore,
|
|
type ServiceInstallOutcome,
|
|
} from '@/services/cmds'
|
|
import { mutate } from '@/services/mutate'
|
|
import { showNotice } from '@/services/notice-service'
|
|
import { useAppDispatch, useRunState } from '@/store/app-store-context'
|
|
|
|
export const ServiceMigrationDialog = () => {
|
|
const { t } = useTranslation()
|
|
const [loading, setLoading] = useState(false)
|
|
const [stateRefreshFailed, setStateRefreshFailed] = useState(false)
|
|
const [workflowIncomplete, setWorkflowIncomplete] = useState(false)
|
|
const dispatch = useAppDispatch()
|
|
const runState = useRunState()
|
|
// Whether the service needs a decision is derived once, in Rust, and travels with the
|
|
// snapshot; a failed refresh is treated as needing one, since we cannot tell otherwise.
|
|
const needsDecision =
|
|
stateRefreshFailed || Boolean(runState?.serviceNeedsAttention)
|
|
// Treat refresh failures as unreachable; an absent Service still needs install after a failed Sidecar attempt.
|
|
const remedy: 'install' | 'repair' | 'reinstall' =
|
|
runState?.pendingAction === 'install'
|
|
? 'install'
|
|
: stateRefreshFailed || runState?.service === 'unavailable'
|
|
? 'repair'
|
|
: runState?.service === 'notInstalled'
|
|
? 'install'
|
|
: 'reinstall'
|
|
const open = loading || workflowIncomplete || needsDecision
|
|
const checking =
|
|
loading ||
|
|
!runState ||
|
|
runState.opInFlight ||
|
|
runState.service === 'unknown'
|
|
const showCheckingMessage = checking || !needsDecision
|
|
const canContinue = Boolean(
|
|
runState &&
|
|
!stateRefreshFailed &&
|
|
(runState.pendingAction === 'install'
|
|
? runState.mode === 'NotRunning' || runState.mode === 'Sidecar'
|
|
: !runState.pendingAction &&
|
|
!runState.sidecarAllowed &&
|
|
runState.mode === 'NotRunning' &&
|
|
(runState.service === 'notInstalled' ||
|
|
runState.service === 'versionMismatch' ||
|
|
runState.service === 'unavailable')),
|
|
)
|
|
|
|
// One store entry to refresh, so there is nothing left to keep coherent by hand.
|
|
const refreshRunState = async () => {
|
|
try {
|
|
const data = await getRuntimeState()
|
|
dispatch({ type: 'runState/loaded', runState: data })
|
|
setStateRefreshFailed(false)
|
|
return data
|
|
} catch (error) {
|
|
setStateRefreshFailed(true)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const handleServiceAction = async () => {
|
|
setLoading(true)
|
|
setWorkflowIncomplete(true)
|
|
let outcome: ServiceInstallOutcome | undefined
|
|
try {
|
|
if (remedy === 'install') {
|
|
const result = await mutate(() => installService(), {
|
|
id: 'install-service',
|
|
errorNotice: false,
|
|
})
|
|
if (result.ok) outcome = result.value
|
|
} else if (remedy === 'repair') {
|
|
const result = await mutate(() => repairService(), {
|
|
id: 'repair-service',
|
|
errorNotice: false,
|
|
})
|
|
if (result.ok) outcome = result.value
|
|
} else {
|
|
const result = await mutate(() => reinstallService(), {
|
|
id: 'reinstall-service',
|
|
errorNotice: false,
|
|
})
|
|
if (result.ok) outcome = result.value
|
|
}
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.actionFailed',
|
|
error,
|
|
)
|
|
}
|
|
|
|
let initialRefreshSucceeded = false
|
|
try {
|
|
await refreshRunState()
|
|
initialRefreshSucceeded = true
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
|
error,
|
|
)
|
|
}
|
|
if (!outcome || !initialRefreshSucceeded) {
|
|
setLoading(false)
|
|
return
|
|
}
|
|
if (outcome.status === 'sidecar') {
|
|
setWorkflowIncomplete(false)
|
|
setLoading(false)
|
|
showNotice.warning(
|
|
'settings.feedback.notifications.clashService.permissionFallback',
|
|
{ reason: outcome.reason },
|
|
0,
|
|
)
|
|
return
|
|
}
|
|
|
|
let restartSucceeded = false
|
|
try {
|
|
const result = await mutate(() => restartCore(), {
|
|
id: 'restart-core',
|
|
errorNotice: false,
|
|
})
|
|
restartSucceeded = result.ok
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.restartFailed',
|
|
error,
|
|
)
|
|
}
|
|
|
|
let finalRefreshSucceeded = false
|
|
try {
|
|
await refreshRunState()
|
|
finalRefreshSucceeded = true
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
|
error,
|
|
)
|
|
}
|
|
if (restartSucceeded && finalRefreshSucceeded) {
|
|
setWorkflowIncomplete(false)
|
|
showNotice.success('layout.components.serviceMigration.success')
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
const handleContinue = async () => {
|
|
setLoading(true)
|
|
setWorkflowIncomplete(true)
|
|
let startupError: unknown
|
|
try {
|
|
await mutate(() => continueWithSidecar(), {
|
|
id: 'continue-with-sidecar',
|
|
errorNotice: false,
|
|
})
|
|
} catch (error) {
|
|
startupError = error
|
|
}
|
|
|
|
let installRefreshSucceeded = false
|
|
try {
|
|
await refreshRunState()
|
|
installRefreshSucceeded = true
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.stateRefreshFailed',
|
|
error,
|
|
)
|
|
}
|
|
if (startupError) {
|
|
showNotice.error(
|
|
'layout.components.serviceMigration.errors.sidecarFailed',
|
|
startupError,
|
|
)
|
|
} else if (installRefreshSucceeded) {
|
|
setWorkflowIncomplete(false)
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<BaseDialog
|
|
open={open}
|
|
title={t('layout.components.serviceMigration.title')}
|
|
okBtn={t(
|
|
remedy === 'install'
|
|
? 'settings.sections.proxyControl.actions.installService'
|
|
: remedy === 'repair'
|
|
? 'layout.components.serviceMigration.repair'
|
|
: 'layout.components.serviceMigration.reinstall',
|
|
)}
|
|
cancelBtn={t('layout.components.serviceMigration.continueSidecar')}
|
|
disableOk={checking}
|
|
disableCancel={checking || !canContinue}
|
|
loading={loading}
|
|
onOk={() => void handleServiceAction()}
|
|
onCancel={() => void handleContinue()}
|
|
>
|
|
<Alert severity="warning">
|
|
{t(
|
|
showCheckingMessage
|
|
? 'layout.components.serviceMigration.checkingMessage'
|
|
: remedy === 'reinstall'
|
|
? 'layout.components.serviceMigration.message'
|
|
: 'layout.components.serviceMigration.unavailableMessage',
|
|
)}
|
|
</Alert>
|
|
</BaseDialog>
|
|
)
|
|
}
|