mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
fix(runtime): keep offline retry toast visible
This commit is contained in:
@@ -78,6 +78,7 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
@@ -207,7 +208,16 @@ describe('runtime-status slice', () => {
|
||||
expect(toast.dismiss).toHaveBeenCalledWith('runtime-environment-disconnected:env-a')
|
||||
})
|
||||
|
||||
it('retries from the toast and re-surfaces it when the server is still offline', async () => {
|
||||
it('keeps the keyed action toast visible when an offline retry settles', async () => {
|
||||
vi.useFakeTimers()
|
||||
const toastId = 'runtime-environment-disconnected:env-a'
|
||||
const visibleToastIds = new Set<string | number>()
|
||||
vi.mocked(toast.warning).mockImplementation((_title, options) => {
|
||||
if (options?.id !== undefined) {
|
||||
visibleToastIds.add(options.id)
|
||||
}
|
||||
return options?.id ?? ''
|
||||
})
|
||||
const getStatus = vi.fn().mockRejectedValue(new Error('closed'))
|
||||
stubRuntimeEnvironmentApi({ getStatus })
|
||||
const store = createSliceStore()
|
||||
@@ -215,18 +225,39 @@ describe('runtime-status slice', () => {
|
||||
store.getState().setRuntimeEnvironmentStatus('env-a', { status: makeStatus(), checkedAt: 1 })
|
||||
store.getState().setRuntimeEnvironmentStatus('env-a', { status: null, checkedAt: 2 })
|
||||
const options = vi.mocked(toast.warning).mock.calls[0]?.[1] as unknown as {
|
||||
action: { onClick: () => void }
|
||||
action: { onClick: (event: { preventDefault: () => void }) => void }
|
||||
}
|
||||
const clickAction = (): { defaultPrevented: boolean } => {
|
||||
const event = {
|
||||
defaultPrevented: false,
|
||||
preventDefault() {
|
||||
this.defaultPrevented = true
|
||||
}
|
||||
}
|
||||
options.action.onClick(event)
|
||||
if (!event.defaultPrevented) {
|
||||
setTimeout(() => visibleToastIds.delete(toastId), 200)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
options.action.onClick()
|
||||
const firstClick = clickAction()
|
||||
const secondClick = clickAction()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getStatus).toHaveBeenCalledWith({ selector: 'env-a', timeoutMs: 10_000 })
|
||||
expect(toast.warning).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
expect(vi.mocked(toast.warning).mock.calls[1]?.[1]?.id).toBe(
|
||||
'runtime-environment-disconnected:env-a'
|
||||
)
|
||||
expect(firstClick.defaultPrevented).toBe(true)
|
||||
expect(secondClick.defaultPrevented).toBe(true)
|
||||
expect(getStatus).toHaveBeenCalledWith({ selector: 'env-a', timeoutMs: 10_000 })
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
expect(visibleToastIds.has(toastId)).toBe(true)
|
||||
expect(getStatus).toHaveBeenCalledTimes(1)
|
||||
expect(toast.warning).toHaveBeenCalledTimes(3)
|
||||
expect(vi.mocked(toast.warning).mock.calls.map((call) => call[1]?.duration)).toEqual([
|
||||
4_000,
|
||||
Number.POSITIVE_INFINITY,
|
||||
4_000
|
||||
])
|
||||
expect(vi.mocked(toast.warning).mock.calls.every((call) => call[1]?.id === toastId)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not report removal or explicit status clearing as a disconnect', () => {
|
||||
|
||||
@@ -64,6 +64,7 @@ export type RuntimeStatusSlice = {
|
||||
|
||||
const connectionGenerationByEnvironment = new Map<string, number>()
|
||||
const activeRuntimeDisconnectedToasts = new Map<string, symbol>()
|
||||
const RUNTIME_DISCONNECTED_TOAST_DURATION_MS = 4_000
|
||||
|
||||
function getRuntimeDisconnectedToastId(environmentId: string): string {
|
||||
return `runtime-environment-disconnected:${environmentId}`
|
||||
@@ -89,30 +90,49 @@ function showRuntimeDisconnectedToast(environmentId: string, getState: () => App
|
||||
activeRuntimeDisconnectedToasts.delete(toastId)
|
||||
}
|
||||
}
|
||||
toast.warning(title, {
|
||||
id: toastId,
|
||||
description: translate(
|
||||
'auto.store.slices.runtime.status.runtimeHostDisconnectedDescription',
|
||||
'Workspaces and terminals on this server are unavailable.'
|
||||
),
|
||||
action: {
|
||||
label: translate('browser.loadFailure.retry', 'Retry'),
|
||||
onClick: () => {
|
||||
void getState()
|
||||
.refreshRuntimeEnvironmentStatus(environmentId)
|
||||
.then((reachable) => {
|
||||
const stillSaved = getState().runtimeEnvironments.some(
|
||||
(entry) => entry.id === environmentId
|
||||
)
|
||||
if (!reachable && stillSaved) {
|
||||
showRuntimeDisconnectedToast(environmentId, getState)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onDismiss: clearActiveToast,
|
||||
onAutoClose: clearActiveToast
|
||||
})
|
||||
let retrying = false
|
||||
const showToast = (duration = RUNTIME_DISCONNECTED_TOAST_DURATION_MS): void => {
|
||||
toast.warning(title, {
|
||||
id: toastId,
|
||||
description: translate(
|
||||
'auto.store.slices.runtime.status.runtimeHostDisconnectedDescription',
|
||||
'Workspaces and terminals on this server are unavailable.'
|
||||
),
|
||||
duration,
|
||||
action: {
|
||||
label: translate('browser.loadFailure.retry', 'Retry'),
|
||||
onClick: (event) => {
|
||||
// Why: Sonner otherwise deletes the keyed toast after the action callback.
|
||||
event.preventDefault()
|
||||
if (retrying) {
|
||||
return
|
||||
}
|
||||
retrying = true
|
||||
showToast(Number.POSITIVE_INFINITY)
|
||||
void getState()
|
||||
.refreshRuntimeEnvironmentStatus(environmentId)
|
||||
.then((reachable) => {
|
||||
const stillSaved = getState().runtimeEnvironments.some(
|
||||
(entry) => entry.id === environmentId
|
||||
)
|
||||
if (
|
||||
!reachable &&
|
||||
stillSaved &&
|
||||
activeRuntimeDisconnectedToasts.get(toastId) === activation
|
||||
) {
|
||||
showToast()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
retrying = false
|
||||
})
|
||||
}
|
||||
},
|
||||
onDismiss: clearActiveToast,
|
||||
onAutoClose: clearActiveToast
|
||||
})
|
||||
}
|
||||
showToast()
|
||||
}
|
||||
|
||||
function dismissRuntimeDisconnectedToast(environmentId: string): void {
|
||||
|
||||
Reference in New Issue
Block a user