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 ( void handleServiceAction()} onCancel={() => void handleContinue()} > {t( showCheckingMessage ? 'layout.components.serviceMigration.checkingMessage' : remedy === 'reinstall' ? 'layout.components.serviceMigration.message' : 'layout.components.serviceMigration.unavailableMessage', )} ) }