Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/App.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ beforeAll(() => configure({ asyncUtilTimeout: 2000 }))
beforeEach(() => clearAccessToken())

test('Show login page', async () => {
const { findByText } = renderApp('/login')
const { findByText } = renderApp('/')
expect(await findByText('Sign in with Google')).not.toBeFalsy()
})

Expand All @@ -32,3 +32,19 @@ describe('Demo mode', () => {
await waitFor(() => expect(video.src).toBeTruthy())
})
})

describe('Public routes', () => {
test('View shared device', async () => {
const { findByText } = renderApp(`/${Demo.DONGLE_ID}`)
expect(await findByText('Not signed in')).toBeTruthy()
expect(await findByText('Shared Device')).toBeTruthy()
})

test('View public route without signing in', async () => {
const { findByText } = renderApp(`/${Demo.DONGLE_ID}/${DEMO_LOG_ID}`)
expect(await findByText(DEMO_LOG_ID)).toBeTruthy()
// Videos do not load, yet
// const video = (await findByTestId('route-video')) as HTMLVideoElement
// await waitFor(() => expect(video.src).toBeTruthy())
})
})
40 changes: 35 additions & 5 deletions src/api/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,44 @@ const sortDevices = (devices: Device[]) =>
}
})

export const getAthenaOfflineQueue = async (dongleId: string) =>
fetcher<AthenaOfflineQueueResponse>(`/v1/devices/${dongleId}/athena_offline_queue`)
const createSharedDevice = (dongleId: string): Device => ({
dongle_id: dongleId,
alias: 'Shared Device',
serial: '',
last_athena_ping: 0,
ignore_uploads: null,
is_paired: true,
is_owner: false,
public_key: '',
prime: false,
prime_type: 0,
trial_claimed: false,
device_type: '',
openpilot_version: '',
sim_id: '',
sim_type: 0,
eligible_features: {
prime: false,
prime_data: false,
nav: false,
},
fetched_at: Math.floor(Date.now() / 1000),
})

export const getDevice = async (dongleId: string) => {
try {
return await fetcher<Device>(`/v1.1/devices/${dongleId}/`)
} catch {
return createSharedDevice(dongleId)
}
}

export const getDevice = async (dongleId: string) => fetcher<Device>(`/v1.1/devices/${dongleId}/`)
export const getAthenaOfflineQueue = async (dongleId: string) =>
fetcher<AthenaOfflineQueueResponse>(`/v1/devices/${dongleId}/athena_offline_queue`).catch(() => null)

export const getDeviceLocation = async (dongleId: string) => fetcher<DeviceLocation>(`/v1/devices/${dongleId}/location`)
export const getDeviceLocation = async (dongleId: string) => fetcher<DeviceLocation>(`/v1/devices/${dongleId}/location`).catch(() => null)

export const getDeviceStats = async (dongleId: string) => fetcher<DrivingStatistics>(`/v1.1/devices/${dongleId}/stats`)
export const getDeviceStats = async (dongleId: string) => fetcher<DrivingStatistics>(`/v1.1/devices/${dongleId}/stats`).catch(() => null)

export const getDevices = async () =>
fetcher<Device[]>('/v1/me/devices/')
Expand Down
2 changes: 1 addition & 1 deletion src/api/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import type { Profile } from '~/types'

import { fetcher } from '.'

export const getProfile = async () => fetcher<Profile>('/v1/me/')
export const getProfile = async () => fetcher<Profile>('/v1/me/').catch(() => null)
13 changes: 8 additions & 5 deletions src/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const parseRouteName = (routeName: string): RouteInfo => {
return { dongleId, routeId }
}

export const getRoute = (routeName: Route['fullname']): Promise<Route> => fetcher<Route>(`/v1/route/${routeName}/`)
export const getRoute = (routeName: Route['fullname']) => fetcher<Route>(`/v1/route/${routeName}/`).catch(() => null)

export const getRouteWithSegments = async (routeName: Route['fullname']) => {
const { dongleId } = parseRouteName(routeName)
Expand All @@ -21,13 +21,15 @@ export const getRouteWithSegments = async (routeName: Route['fullname']) => {
return routes[0]
}

export const getRouteShareSignature = (routeName: string): Promise<RouteShareSignature> => fetcher(`/v1/route/${routeName}/share_signature`)
export const getRouteShareSignature = (routeName: string) => fetcher<RouteShareSignature>(`/v1/route/${routeName}/share_signature`)

export const createQCameraStreamUrl = (routeName: Route['fullname'], signature: RouteShareSignature): string =>
`${BASE_URL}/v1/route/${routeName}/qcamera.m3u8?${new URLSearchParams(signature).toString()}`

export const getQCameraStreamUrl = (routeName: Route['fullname']): Promise<string> =>
getRouteShareSignature(routeName).then((signature) => createQCameraStreamUrl(routeName, signature))
export const getQCameraStreamUrl = (routeName: Route['fullname']) =>
getRouteShareSignature(routeName)
.then((signature) => createQCameraStreamUrl(routeName, signature))
.catch(() => null)

export const setRoutePublic = (routeName: string, isPublic: boolean): Promise<Route> =>
fetcher<Route>(`/v1/route/${routeName}/`, {
Expand All @@ -38,7 +40,8 @@ export const setRoutePublic = (routeName: string, isPublic: boolean): Promise<Ro
body: JSON.stringify({ is_public: isPublic }),
})

export const getPreservedRoutes = (dongleId: string): Promise<Route[]> => fetcher<Route[]>(`/v1/devices/${dongleId}/routes/preserved`)
export const getPreservedRoutes = (dongleId: string): Promise<Route[]> =>
fetcher<Route[]>(`/v1/devices/${dongleId}/routes/preserved`).catch(() => [])

export const setRoutePreserved = (routeName: string, preserved: boolean): Promise<Route> =>
fetcher<Route>(`/v1/route/${routeName}/preserve`, { method: preserved ? 'POST' : 'DELETE' })
4 changes: 2 additions & 2 deletions src/components/RouteStatistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const formatEngagement = (timeline?: TimelineStatistics): string | undefined =>
return `${(100 * (engagedDuration / duration)).toFixed(0)}%`
}

const RouteStatistics: VoidComponent<{ class?: string; route: Route }> = (props) => {
const RouteStatistics: VoidComponent<{ class?: string; route?: Route | null }> = (props) => {
const [timeline] = createResource(() => props.route, getTimelineStatistics)

return (
<StatisticBar
class={props.class}
statistics={[
{ label: 'Distance', value: () => formatDistance(props.route?.length) },
{ label: 'Duration', value: () => formatRouteDuration(props.route) },
{ label: 'Duration', value: () => (props.route ? formatRouteDuration(props.route) : undefined) },
{ label: 'Engaged', value: () => formatEngagement(timeline()) },
]}
/>
Expand Down
1 change: 1 addition & 0 deletions src/components/material/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const Icons = [
'add', 'arrow_back', 'camera', 'check', 'chevron_right', 'clear', 'close', 'delete', 'description', 'directions_car', 'download', 'error',
'file_copy', 'flag', 'info', 'keyboard_arrow_down', 'keyboard_arrow_up', 'local_fire_department', 'logout', 'menu', 'my_location',
'open_in_new', 'payments', 'person', 'progress_activity', 'satellite_alt', 'search', 'settings', 'sync', 'upload', 'videocam', 'refresh',
'login', 'person_off'
] as const

export type IconName = (typeof Icons)[number]
Expand Down
31 changes: 21 additions & 10 deletions src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,25 @@ const DashboardDrawer: VoidComponent = () => {
<Suspense fallback={<div class="min-h-16 rounded-md skeleton-loader" />}>
<div class="flex max-w-full items-center px-3 rounded-md outline outline-1 outline-outline-variant min-h-16">
<div class="shrink-0 size-10 inline-flex items-center justify-center rounded-full bg-primary-container text-on-primary-container">
<Icon name="person" filled />
<Icon name={profile.latest === null ? 'person_off' : 'person'} filled />
</div>
<div class="min-w-0 mx-3">
<div class="truncate text-body-md text-on-surface">{profile()?.email}</div>
<div class="truncate text-label-sm text-on-surface-variant">{profile()?.user_id}</div>
</div>
<div class="grow" />
<IconButton name="logout" href="/logout" />
<Show
when={profile()}
fallback={
<>
<div class="mx-3">Not signed in</div>
<div class="grow" />
<IconButton name="login" href="/login" />
</>
}
>
<div class="min-w-0 mx-3">
<div class="truncate text-body-md text-on-surface">{profile()?.email}</div>
<div class="truncate text-label-sm text-on-surface-variant">{profile()?.user_id}</div>
</div>
<div class="grow" />
<IconButton name="logout" href="/logout" />
</Show>
</div>
</Suspense>
</div>
Expand Down Expand Up @@ -112,9 +123,6 @@ const Dashboard: Component<RouteSectionProps> = () => {
return (
<Drawer drawer={<DashboardDrawer />}>
<Switch fallback={<TopAppBar leading={<DrawerToggleButton />}>No device</TopAppBar>}>
<Match when={!!profile.error}>
<Navigate href="/login" />
</Match>
<Match when={dongleId() === 'pair' || pairToken()}>
<PairActivity />
</Match>
Expand Down Expand Up @@ -143,6 +151,9 @@ const Dashboard: Component<RouteSectionProps> = () => {
/>
)}
</Match>
<Match when={profile() === null}>
<Navigate href="/login" />
</Match>
<Match when={getDefaultDongleId()} keyed>
{(defaultDongleId) => <Navigate href={`/${defaultDongleId}`} />}
</Match>
Expand Down
21 changes: 12 additions & 9 deletions src/pages/dashboard/activities/DeviceActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
const [device] = createResource(() => props.dongleId, getDevice)
const [deviceName] = createResource(device, getDeviceName)
const [queueVisible, setQueueVisible] = createSignal(false)
const [isDeviceOwner] = createResource(device, (device) => device.is_owner)
const [snapshot, setSnapshot] = createSignal<{
error: string | null
fetching: boolean
Expand Down Expand Up @@ -113,16 +114,18 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
<Show when={deviceName()} fallback={<div class="skeleton-loader size-full" />}>
<DeviceLocation dongleId={props.dongleId} deviceName={deviceName()!} />
</Show>
<div class="flex">
<div class="flex-auto">
<Suspense fallback={<div class="skeleton-loader size-full" />}>
<DeviceStatistics dongleId={props.dongleId} class="p-4" />
</Suspense>
</div>
<div class="flex p-4">
<IconButton name="camera" onClick={() => void takeSnapshot()} />
<Show when={isDeviceOwner()}>
<div class="flex">
<div class="flex-auto">
<Suspense fallback={<div class="skeleton-loader size-full" />}>
<DeviceStatistics dongleId={props.dongleId} class="p-4" />
</Suspense>
</div>
<div class="flex p-4">
<IconButton name="camera" onClick={() => void takeSnapshot()} />
</div>
</div>
</div>
</Show>
<Show when={queueVisible()}>
<UploadQueue dongleId={props.dongleId} />
</Show>
Expand Down
Loading