mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
refactor(mobile): run both connection log routes on one screen
The native route inlined 245 lines of orchestration the hosted screen had already factored behind DiagnosticsDeviceOperations, so the two paths gathered, redacted, and submitted the same report differently. The screen now takes the device operations, a clipboard writer, and a host picker slot; the native route supplies the existing native operations and its host chips. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
+42
-204
@@ -1,39 +1,18 @@
|
||||
import { ConnectionDiagnosticsView } from '../src/diagnostics/connection-diagnostics-view'
|
||||
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
||||
import { View, Text, Pressable, Platform } from 'react-native'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { View, Text, Pressable } from 'react-native'
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import Constants from 'expo-constants'
|
||||
import { loadHosts } from '../src/transport/host-store'
|
||||
import { connectionLogStore } from '../src/transport/persisted-connection-log-store'
|
||||
import { useHostClient, useRpcClientContext } from '../src/transport/client-context'
|
||||
import {
|
||||
useConnectionPathStatus,
|
||||
useReconnectAttempt
|
||||
} from '../src/transport/client-context-connection-metrics'
|
||||
import { buildConnectionDiagnosticsReport } from '../src/diagnostics/connection-diagnostics-report'
|
||||
import { mobileWebDiagnosticsStore } from '../src/mobile-web/mobile-web-diagnostics-store'
|
||||
import {
|
||||
diagnoseConnection,
|
||||
getReportableConnectionIncidentId
|
||||
} from '../src/diagnostics/connection-diagnostics-analysis'
|
||||
import { submitConnectionDiagnostics } from '../src/diagnostics/connection-diagnostics-submission'
|
||||
import {
|
||||
readHydratedConnectionLog,
|
||||
readConnectionDiagnosticsSnapshot,
|
||||
resolveDiagnosticsHostId,
|
||||
getDiagnosticsSubmissionState,
|
||||
updateDiagnosticsSubmissionState,
|
||||
type DiagnosticsSubmissionStates
|
||||
} from '../src/diagnostics/connection-diagnostics-screen-data'
|
||||
import { useHostStatusGates } from '../src/transport/host-status-gates'
|
||||
import { loadHostAppVersion } from '../src/transport/host-app-version-store'
|
||||
import { ConnectionDiagnosticsScreen } from '../src/diagnostics/connection-diagnostics-screen'
|
||||
import { createNativeDiagnosticsOperations } from '../src/diagnostics/native-diagnostics-operations'
|
||||
import {
|
||||
resolveDiagnosticsHostId,
|
||||
type DiagnosticsHostSelection
|
||||
} from '../src/diagnostics/connection-diagnostics-screen-data'
|
||||
import { connectionDiagnosticsScreenStyles as styles } from '../src/diagnostics/connection-diagnostics-screen-styles'
|
||||
import type { ConnectionLogEntry, HostProfile } from '../src/transport/types'
|
||||
|
||||
// Why: getSnapshot must be referentially stable when there's no data —
|
||||
// a fresh [] per call would make useSyncExternalStore re-render forever.
|
||||
const EMPTY_ENTRIES: readonly ConnectionLogEntry[] = []
|
||||
import type { HostProfile } from '../src/transport/types'
|
||||
|
||||
// Why: reading the log is most needed while a host is failing, so this
|
||||
// screen also *acquires* the host client — opening it kicks a dial and the
|
||||
@@ -44,21 +23,14 @@ export default function ConnectionLogScreen() {
|
||||
const params = useLocalSearchParams<{ hostId?: string }>()
|
||||
const routeKey = useMemo(() => ({}), [params.hostId])
|
||||
const [hosts, setHosts] = useState<HostProfile[]>([])
|
||||
const [manualSelection, setManualSelection] = useState<{
|
||||
hostId: string
|
||||
requestedHostId: string | undefined
|
||||
routeKey: object
|
||||
} | null>(null)
|
||||
const [copiedHostId, setCopiedHostId] = useState<string | null>(null)
|
||||
const [submissionStates, setSubmissionStates] = useState<DiagnosticsSubmissionStates>({})
|
||||
const [manualSelection, setManualSelection] = useState<DiagnosticsHostSelection | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
void loadHosts().then((loaded) => {
|
||||
if (stale) {
|
||||
return
|
||||
if (!stale) {
|
||||
setHosts(loaded)
|
||||
}
|
||||
setHosts(loaded)
|
||||
})
|
||||
return () => {
|
||||
stale = true
|
||||
@@ -66,179 +38,45 @@ export default function ConnectionLogScreen() {
|
||||
}, [])
|
||||
|
||||
const selectedId = resolveDiagnosticsHostId(hosts, params.hostId, manualSelection, routeKey)
|
||||
const selected = hosts.find((h) => h.id === selectedId) ?? null
|
||||
const selected = hosts.find((host) => host.id === selectedId) ?? null
|
||||
const { client, state } = useHostClient(selected?.id)
|
||||
const { desktopAppVersion: liveDesktopAppVersion } = useHostStatusGates({
|
||||
hostId: selected?.id,
|
||||
client,
|
||||
connState: state
|
||||
})
|
||||
const reconnectAttempts = useReconnectAttempt(selected?.id)
|
||||
const { activePath, pendingPath } = useConnectionPathStatus(selected?.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId) {
|
||||
void readHydratedConnectionLog(connectionLogStore, selectedId)
|
||||
}
|
||||
}, [selectedId])
|
||||
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) =>
|
||||
selectedId ? connectionLogStore.subscribe(selectedId, listener) : () => {},
|
||||
[selectedId]
|
||||
// Why: refreshes the persisted desktop version the diagnostics report reads back.
|
||||
useHostStatusGates({ hostId: selected?.id, client, connState: state })
|
||||
const device = useMemo(
|
||||
() => (selected ? createNativeDiagnosticsOperations(selected, clientContext) : null),
|
||||
[selected, clientContext]
|
||||
)
|
||||
const getSnapshot = useCallback(
|
||||
() => (selectedId ? connectionLogStore.get(selectedId) : EMPTY_ENTRIES),
|
||||
[selectedId]
|
||||
const select = useCallback(
|
||||
(hostId: string) => setManualSelection({ hostId, requestedHostId: params.hostId, routeKey }),
|
||||
[params.hostId, routeKey]
|
||||
)
|
||||
const entries = useSyncExternalStore(subscribe, getSnapshot)
|
||||
const subscribeMobileWeb = useCallback(
|
||||
(listener: () => void) => mobileWebDiagnosticsStore.subscribe(listener),
|
||||
[]
|
||||
)
|
||||
const getMobileWebSnapshot = useCallback(
|
||||
() => mobileWebDiagnosticsStore.get(selectedId),
|
||||
[selectedId]
|
||||
)
|
||||
const mobileWebDiagnostics = useSyncExternalStore(subscribeMobileWeb, getMobileWebSnapshot)
|
||||
const diagnosis = selected
|
||||
? diagnoseConnection({ endpoint: selected.endpoint, state, activePath, pendingPath, entries })
|
||||
: null
|
||||
const incidentId = selected
|
||||
? getReportableConnectionIncidentId({
|
||||
endpoint: selected.endpoint,
|
||||
state,
|
||||
activePath,
|
||||
pendingPath,
|
||||
entries
|
||||
})
|
||||
: null
|
||||
const submissionKey = selected && incidentId ? `${selected.id}:${incidentId}` : null
|
||||
const submissionState = getDiagnosticsSubmissionState(submissionStates, submissionKey)
|
||||
const copied = copiedHostId === selectedId
|
||||
|
||||
const copyDiagnostics = useCallback(async () => {
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
const desktopAppVersion = liveDesktopAppVersion ?? (await loadHostAppVersion(selected.id))
|
||||
const snapshot = await readConnectionDiagnosticsSnapshot(
|
||||
clientContext,
|
||||
connectionLogStore,
|
||||
selected.id
|
||||
)
|
||||
const report = buildConnectionDiagnosticsReport({
|
||||
endpoint: selected.endpoint,
|
||||
state: snapshot.state,
|
||||
reconnectAttempts: snapshot.reconnectAttempts,
|
||||
lastConnectedAt: snapshot.lastConnectedAt,
|
||||
platform: `${Platform.OS} ${Platform.Version ?? ''}`.trim(),
|
||||
appVersion: Constants.expoConfig?.version ?? 'unknown',
|
||||
desktopAppVersion,
|
||||
entries: snapshot.entries,
|
||||
activePath: snapshot.activePath,
|
||||
pendingPath: snapshot.pendingPath,
|
||||
mobileWeb: mobileWebDiagnostics
|
||||
})
|
||||
await Clipboard.setStringAsync(report)
|
||||
setCopiedHostId(selected.id)
|
||||
setTimeout(() => setCopiedHostId((hostId) => (hostId === selected.id ? null : hostId)), 2000)
|
||||
}, [selected, liveDesktopAppVersion, clientContext, mobileWebDiagnostics])
|
||||
|
||||
const sendDiagnostics = useCallback(async () => {
|
||||
if (!selected || !submissionKey || submissionState === 'sending') {
|
||||
return
|
||||
}
|
||||
const startedKey = submissionKey
|
||||
setSubmissionStates((states) => updateDiagnosticsSubmissionState(states, startedKey, 'sending'))
|
||||
const appVersion = Constants.expoConfig?.version ?? 'unknown'
|
||||
const platform = `${Platform.OS} ${Platform.Version ?? ''}`.trim()
|
||||
const desktopAppVersion = liveDesktopAppVersion ?? (await loadHostAppVersion(selected.id))
|
||||
const snapshot = await readConnectionDiagnosticsSnapshot(
|
||||
clientContext,
|
||||
connectionLogStore,
|
||||
selected.id
|
||||
)
|
||||
const currentIncidentId = getReportableConnectionIncidentId({
|
||||
endpoint: selected.endpoint,
|
||||
state: snapshot.state,
|
||||
activePath: snapshot.activePath,
|
||||
pendingPath: snapshot.pendingPath,
|
||||
entries: snapshot.entries
|
||||
})
|
||||
if (`${selected.id}:${currentIncidentId ?? ''}` !== startedKey) {
|
||||
setSubmissionStates((states) => updateDiagnosticsSubmissionState(states, startedKey, null))
|
||||
return
|
||||
}
|
||||
const report = buildConnectionDiagnosticsReport({
|
||||
endpoint: selected.endpoint,
|
||||
state: snapshot.state,
|
||||
reconnectAttempts: snapshot.reconnectAttempts,
|
||||
lastConnectedAt: snapshot.lastConnectedAt,
|
||||
platform,
|
||||
appVersion,
|
||||
desktopAppVersion,
|
||||
entries: snapshot.entries,
|
||||
activePath: snapshot.activePath,
|
||||
pendingPath: snapshot.pendingPath,
|
||||
mobileWeb: mobileWebDiagnostics
|
||||
})
|
||||
const result = await submitConnectionDiagnostics({ report, appVersion, platform })
|
||||
setSubmissionStates((states) =>
|
||||
updateDiagnosticsSubmissionState(states, startedKey, result.ok ? 'sent' : 'failed')
|
||||
)
|
||||
}, [
|
||||
selected,
|
||||
submissionKey,
|
||||
submissionState,
|
||||
liveDesktopAppVersion,
|
||||
clientContext,
|
||||
mobileWebDiagnostics
|
||||
])
|
||||
|
||||
return (
|
||||
<ConnectionDiagnosticsView
|
||||
<ConnectionDiagnosticsScreen
|
||||
key={selectedId ?? 'none'}
|
||||
device={device}
|
||||
hostName={selected?.name ?? null}
|
||||
state={state}
|
||||
reconnectAttempts={reconnectAttempts}
|
||||
copied={copied}
|
||||
copyDiagnostics={copyDiagnostics}
|
||||
diagnosis={diagnosis}
|
||||
submissionState={submissionState}
|
||||
sendDiagnostics={sendDiagnostics}
|
||||
entries={entries}
|
||||
writeClipboard={(report) => Clipboard.setStringAsync(report)}
|
||||
onBack={() => router.back()}
|
||||
hostPicker={
|
||||
<>
|
||||
{' '}
|
||||
{hosts.length > 1 && (
|
||||
<View style={styles.hostPicker}>
|
||||
{hosts.map((host) => (
|
||||
<Pressable
|
||||
key={host.id}
|
||||
style={[styles.hostChip, host.id === selectedId && styles.hostChipActive]}
|
||||
onPress={() =>
|
||||
setManualSelection({
|
||||
hostId: host.id,
|
||||
requestedHostId: params.hostId,
|
||||
routeKey
|
||||
})
|
||||
}
|
||||
hosts.length > 1 ? (
|
||||
<View style={styles.hostPicker}>
|
||||
{hosts.map((host) => (
|
||||
<Pressable
|
||||
key={host.id}
|
||||
style={[styles.hostChip, host.id === selectedId && styles.hostChipActive]}
|
||||
onPress={() => select(host.id)}
|
||||
>
|
||||
<Text
|
||||
style={[styles.hostChipText, host.id === selectedId && styles.hostChipTextActive]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.hostChipText,
|
||||
host.id === selectedId && styles.hostChipTextActive
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{host.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
{host.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useMobileWebNativeShell } from '../../src/mobile-web/src/native-shell-channel'
|
||||
import { HostedConnectionDiagnosticsScreen } from '../src/diagnostics/hosted-connection-diagnostics-screen'
|
||||
import { ConnectionDiagnosticsScreen } from '../src/diagnostics/connection-diagnostics-screen'
|
||||
|
||||
export default function HostedConnectionLogRoute() {
|
||||
const router = useRouter()
|
||||
@@ -8,10 +8,13 @@ export default function HostedConnectionLogRoute() {
|
||||
if (!shell.client) {
|
||||
return null
|
||||
}
|
||||
const client = shell.client
|
||||
return (
|
||||
<HostedConnectionDiagnosticsScreen
|
||||
<ConnectionDiagnosticsScreen
|
||||
key={shell.context?.shellSessionId ?? 'pending'}
|
||||
client={shell.client}
|
||||
device={client.native.diagnosticsDevice}
|
||||
hostName="Paired desktop"
|
||||
writeClipboard={(report) => client.native.clipboardWrite(report)}
|
||||
onBack={() => (router.canGoBack() ? router.back() : router.replace('/settings'))}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { createElement, useEffect } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ConnectionDiagnosticsScreen } from './connection-diagnostics-screen'
|
||||
import type { DiagnosticsDeviceOperations } from './diagnostics-device-operations'
|
||||
import type { DiagnosticsSnapshot } from '../../../src/shared/mobile-web/diagnostics-device-contract'
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
View: 'View',
|
||||
Text: 'Text',
|
||||
Pressable: 'Pressable',
|
||||
ScrollView: 'ScrollView',
|
||||
StyleSheet: { create: (value: unknown) => value, hairlineWidth: 1 }
|
||||
}))
|
||||
vi.mock('react-native-safe-area-context', () => ({
|
||||
useSafeAreaInsets: () => ({ top: 0, bottom: 0 })
|
||||
}))
|
||||
vi.mock('expo-router', () => ({
|
||||
useFocusEffect: (callback: () => void | (() => void)) => useEffect(callback, [callback])
|
||||
}))
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
ChevronLeft: 'Icon',
|
||||
Copy: 'Icon',
|
||||
Check: 'Icon',
|
||||
Send: 'Icon'
|
||||
}))
|
||||
vi.mock('../components/ConnectionLog', () => ({ ConnectionLog: () => null }))
|
||||
|
||||
const SNAPSHOT: DiagnosticsSnapshot = {
|
||||
state: 'disconnected',
|
||||
reconnectAttempts: 3,
|
||||
lastConnectedAt: null,
|
||||
activePath: 'lan',
|
||||
pendingPath: null,
|
||||
endpointIsTailscale: false,
|
||||
platform: 'ios 18',
|
||||
appVersion: '1.0.0',
|
||||
desktopAppVersion: '2.0.0',
|
||||
entries: [],
|
||||
mobileWeb: {}
|
||||
}
|
||||
|
||||
let renderer: ReactTestRenderer | undefined
|
||||
afterEach(() => {
|
||||
act(() => renderer?.unmount())
|
||||
renderer = undefined
|
||||
})
|
||||
|
||||
function device(overrides: Partial<DiagnosticsDeviceOperations> = {}) {
|
||||
return {
|
||||
snapshot: vi.fn().mockResolvedValue(SNAPSHOT),
|
||||
probe: vi.fn().mockResolvedValue({ reachable: true }),
|
||||
submit: vi.fn().mockResolvedValue({ ok: true }),
|
||||
...overrides
|
||||
} as unknown as DiagnosticsDeviceOperations
|
||||
}
|
||||
|
||||
async function mount(props: Parameters<typeof ConnectionDiagnosticsScreen>[0]) {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(ConnectionDiagnosticsScreen, props))
|
||||
})
|
||||
}
|
||||
|
||||
function texts(): string[] {
|
||||
return renderer!.root
|
||||
.findAllByType('Text')
|
||||
.flatMap((node) => node.children.filter((child): child is string => typeof child === 'string'))
|
||||
}
|
||||
|
||||
describe('shared connection diagnostics screen', () => {
|
||||
it('reports either host through one device operations seam', async () => {
|
||||
const write = vi.fn().mockResolvedValue(undefined)
|
||||
const operations = device()
|
||||
await mount({
|
||||
device: operations,
|
||||
hostName: 'Desk',
|
||||
writeClipboard: write,
|
||||
onBack: () => {}
|
||||
})
|
||||
|
||||
expect(texts()).toContain('disconnected')
|
||||
expect(texts()).toContain(' · attempt 3')
|
||||
expect(operations.snapshot).toHaveBeenCalled()
|
||||
await act(async () => {
|
||||
await renderer!.root.findAllByType('Pressable')[1]!.props.onPress()
|
||||
})
|
||||
expect(write).toHaveBeenCalledWith(expect.stringContaining('2.0.0'))
|
||||
})
|
||||
|
||||
it('surfaces a device failure instead of a permanent loading state', async () => {
|
||||
await mount({
|
||||
device: device({ snapshot: vi.fn().mockRejectedValue(new Error('unreachable')) }),
|
||||
hostName: 'Desk',
|
||||
writeClipboard: vi.fn(),
|
||||
onBack: () => {}
|
||||
})
|
||||
|
||||
expect(texts().some((text) => text.includes('Could not load network diagnostics'))).toBe(true)
|
||||
})
|
||||
|
||||
it('renders no host rather than loading when no host is selected', async () => {
|
||||
await mount({ device: null, hostName: null, writeClipboard: vi.fn(), onBack: () => {} })
|
||||
|
||||
expect(texts()).not.toContain('Loading network diagnostics…')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useState, type ReactNode } from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import { useFocusEffect } from 'expo-router'
|
||||
import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client'
|
||||
import type { DiagnosticsSnapshot } from '../../../src/shared/mobile-web/diagnostics-device-contract'
|
||||
import type { ConnectionLogEntry } from '../transport/types'
|
||||
import type { MobileWebDiagnosticsSnapshot } from '../mobile-web/mobile-web-diagnostics-store'
|
||||
@@ -17,21 +16,40 @@ import {
|
||||
type DiagnosticsSubmissionStates
|
||||
} from './connection-diagnostics-screen-data'
|
||||
import { connectionDiagnosticsScreenStyles as styles } from './connection-diagnostics-screen-styles'
|
||||
import type { DiagnosticsDeviceOperations } from './diagnostics-device-operations'
|
||||
|
||||
export function HostedConnectionDiagnosticsScreen({
|
||||
client,
|
||||
onBack
|
||||
function reportable(snapshot: DiagnosticsSnapshot) {
|
||||
return {
|
||||
...snapshot,
|
||||
entries: snapshot.entries as ConnectionLogEntry[],
|
||||
mobileWeb: snapshot.mobileWeb as MobileWebDiagnosticsSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
// Why: reading the log matters most while a host is failing, so the screen
|
||||
// re-polls the device instead of rendering a snapshot taken on mount.
|
||||
export function ConnectionDiagnosticsScreen({
|
||||
device,
|
||||
hostName,
|
||||
writeClipboard,
|
||||
onBack,
|
||||
hostPicker
|
||||
}: {
|
||||
client: MobileWebBridgeClient
|
||||
device: DiagnosticsDeviceOperations | null
|
||||
hostName: string | null
|
||||
writeClipboard: (report: string) => Promise<unknown>
|
||||
onBack: () => void
|
||||
hostPicker?: ReactNode
|
||||
}) {
|
||||
const device = client.native.diagnosticsDevice
|
||||
const [snapshot, setSnapshot] = useState<DiagnosticsSnapshot | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [submissions, setSubmissions] = useState<DiagnosticsSubmissionStates>({})
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
let pending = false
|
||||
const refresh = async () => {
|
||||
@@ -61,44 +79,31 @@ export function HostedConnectionDiagnosticsScreen({
|
||||
}
|
||||
}, [device])
|
||||
)
|
||||
const data = snapshot
|
||||
? {
|
||||
...snapshot,
|
||||
entries: snapshot.entries as ConnectionLogEntry[],
|
||||
mobileWeb: snapshot.mobileWeb as MobileWebDiagnosticsSnapshot
|
||||
}
|
||||
: null
|
||||
const data = snapshot ? reportable(snapshot) : null
|
||||
const diagnosis = data ? diagnoseConnection(data) : null
|
||||
const incident = data ? getReportableConnectionIncidentId(data) : null
|
||||
const submissionState = getDiagnosticsSubmissionState(submissions, incident)
|
||||
const copyDiagnostics = async () => {
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const fresh = await device.snapshot()
|
||||
await client.native.clipboardWrite(
|
||||
buildConnectionDiagnosticsReport({
|
||||
...fresh,
|
||||
entries: fresh.entries as ConnectionLogEntry[],
|
||||
mobileWeb: fresh.mobileWeb as MobileWebDiagnosticsSnapshot
|
||||
})
|
||||
)
|
||||
await writeClipboard(buildConnectionDiagnosticsReport(reportable(await device.snapshot())))
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
setError('Could not copy the report. Try again.')
|
||||
}
|
||||
}
|
||||
const sendDiagnostics = async () => {
|
||||
if (!incident || submissionState === 'sending') {
|
||||
if (!device || !incident || submissionState === 'sending') {
|
||||
return
|
||||
}
|
||||
const started = incident
|
||||
setSubmissions((states) => updateDiagnosticsSubmissionState(states, started, 'sending'))
|
||||
try {
|
||||
const fresh = await device.snapshot()
|
||||
const reportData = {
|
||||
...fresh,
|
||||
entries: fresh.entries as ConnectionLogEntry[],
|
||||
mobileWeb: fresh.mobileWeb as MobileWebDiagnosticsSnapshot
|
||||
}
|
||||
const reportData = reportable(fresh)
|
||||
if (getReportableConnectionIncidentId(reportData) !== started) {
|
||||
setSubmissions((states) => updateDiagnosticsSubmissionState(states, started, null))
|
||||
return
|
||||
@@ -117,8 +122,8 @@ export function HostedConnectionDiagnosticsScreen({
|
||||
}
|
||||
return (
|
||||
<ConnectionDiagnosticsView
|
||||
loading={!snapshot}
|
||||
hostName="Paired desktop"
|
||||
loading={Boolean(device) && !snapshot}
|
||||
hostName={hostName}
|
||||
state={snapshot?.state ?? 'disconnected'}
|
||||
reconnectAttempts={snapshot?.reconnectAttempts ?? 0}
|
||||
entries={data?.entries ?? []}
|
||||
@@ -129,11 +134,14 @@ export function HostedConnectionDiagnosticsScreen({
|
||||
sendDiagnostics={sendDiagnostics}
|
||||
onBack={onBack}
|
||||
hostPicker={
|
||||
error ? (
|
||||
<Text accessibilityRole="alert" style={styles.emptyText}>
|
||||
{error}
|
||||
</Text>
|
||||
) : null
|
||||
<>
|
||||
{hostPicker}
|
||||
{error ? (
|
||||
<Text accessibilityRole="alert" style={styles.emptyText}>
|
||||
{error}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user