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
2 changes: 1 addition & 1 deletion app/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { ErrorBoundary as BaseErrorBoundary } from 'react-error-boundary'
import { useRouteError } from 'react-router-dom'

import type { ApiError } from '@oxide/api'
import { type ApiError } from '~/api/errors'

import { ErrorPage, NotFound } from './ErrorPage'

Expand Down
23 changes: 22 additions & 1 deletion app/components/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { Link } from 'react-router-dom'

import { Error12Icon, PrevArrow12Icon } from '@oxide/design-system/icons/react'

import { useApiMutation } from '~/api/client'
import { navToLogin } from '~/api/nav-to-login'
import { Button } from '~/ui/lib/Button'

const GradientBackground = () => (
<div
// negative z-index avoids covering MSW warning banner
Expand All @@ -27,14 +31,15 @@ export function ErrorPage({ children }: Props) {
return (
<div className="flex w-full justify-center">
<GradientBackground />
<div className="relative w-full">
<div className="relative flex w-full justify-between">
<Link
to="/"
className="flex items-center p-6 text-mono-sm text-secondary hover:text-default"
>
<PrevArrow12Icon title="Select" className="mr-2 text-tertiary" />
Back to console
</Link>
<SignOutButton className="mr-6 mt-4" />
</div>
<div className="absolute left-1/2 top-1/2 flex w-96 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center space-y-4 rounded-lg border p-8 !bg-raise border-secondary elevation-3">
<div className="my-2 flex h-12 w-12 items-center justify-center">
Expand All @@ -58,3 +63,19 @@ export function NotFound() {
</ErrorPage>
)
}

export function SignOutButton({ className }: { className?: string }) {
const logout = useApiMutation('logout', {
onSuccess: () => navToLogin({ includeCurrent: false }),
})
return (
<Button
onClick={() => logout.mutate({})}
className={className}
size="sm"
variant="ghost"
>
Sign out
</Button>
)
}
7 changes: 1 addition & 6 deletions app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ import { pb } from '~/util/path-builder'
export function TopBar({ children }: { children: React.ReactNode }) {
const navigate = useNavigate()
const logout = useApiMutation('logout', {
onSuccess: () => {
// server will respond to /login with a login redirect
// TODO-usability: do we just want to dump them back to login or is there
// another page that would make sense, like a logged out homepage
navToLogin({ includeCurrent: false })
},
onSuccess: () => navToLogin({ includeCurrent: false }),
})
// fetch happens in loader wrapping all authed pages
const { me } = useCurrentUser()
Expand Down
7 changes: 5 additions & 2 deletions mock-api/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
currentUser,
errIfExists,
errIfInvalidDiskSize,
forbiddenErr,
getStartAndEndTime,
getTimestamps,
handleMetrics,
Expand All @@ -51,6 +52,7 @@ import {
// is *JSON type.

export const handlers = makeHandlers({
logout: () => 204,
ping: () => ({ status: 'ok' }),
deviceAuthRequest: () => 200,
deviceAuthConfirm: ({ body }) => (body.user_code === 'ERRO-RABC' ? 400 : 200),
Expand All @@ -74,7 +76,9 @@ export const handlers = makeHandlers({
},
projectView: ({ path }) => {
if (path.project.endsWith('error-503')) {
throw unavailableErr
throw unavailableErr()
} else if (path.project.endsWith('error-403')) {
throw forbiddenErr()
}

return lookup.project({ ...path })
Expand Down Expand Up @@ -1263,7 +1267,6 @@ export const handlers = makeHandlers({
localIdpUserDelete: NotImplemented,
localIdpUserSetPassword: NotImplemented,
loginSaml: NotImplemented,
logout: NotImplemented,
networkingAddressLotBlockList: NotImplemented,
networkingAddressLotCreate: NotImplemented,
networkingAddressLotDelete: NotImplemented,
Expand Down
3 changes: 3 additions & 0 deletions mock-api/msw/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export function getTimestamps() {
return { time_created: now, time_modified: now }
}

export const forbiddenErr = () =>
json({ error_code: 'Forbidden', request_id: 'fake-id' }, { status: 403 })

export const unavailableErr = () =>
json({ error_code: 'ServiceUnavailable', request_id: 'fake-id' }, { status: 503 })

Expand Down
10 changes: 10 additions & 0 deletions test/e2e/error-pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ test('Shows 404 page when a resource is not found', async ({ page }) => {

await page.goto('/projects/nonexistent')
await expect(page.locator('text=Page not found')).toBeVisible()

await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible()
})

test('Shows something went wrong page on other errors', async ({ page }) => {
Expand All @@ -31,4 +33,12 @@ test('Shows something went wrong page on other errors', async ({ page }) => {
const error =
'Invariant failed: Expected query to be prefetched. Key: ["projectView",{"path":{"project":"error-503"}}]'
expect(errors.some((e) => e.message.includes(error))).toBeTruthy()

// test clicking sign out
await page.getByRole('button', { name: 'Sign out' }).click()
// login route doesn't actually work in the mock setup (in production this
// is handled by nexus, and it's hard to get Vite to do the right thing here
// without getting elaborate with middleware), so this is a 404, but we do end
// up at the right URL
await expect(page).toHaveURL('/login')
})