Skip to content

Commit b815dd8

Browse files
authored
Add logout button to error page (#2244)
* add logout button to error page * move sign out button to the corner, show on all error pages
1 parent 8c7b294 commit b815dd8

File tree

6 files changed

+42
-10
lines changed

6 files changed

+42
-10
lines changed

app/components/ErrorBoundary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { ErrorBoundary as BaseErrorBoundary } from 'react-error-boundary'
99
import { useRouteError } from 'react-router-dom'
1010

11-
import type { ApiError } from '@oxide/api'
11+
import { type ApiError } from '~/api/errors'
1212

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

app/components/ErrorPage.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { Link } from 'react-router-dom'
1010

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

13+
import { useApiMutation } from '~/api/client'
14+
import { navToLogin } from '~/api/nav-to-login'
15+
import { Button } from '~/ui/lib/Button'
16+
1317
const GradientBackground = () => (
1418
<div
1519
// negative z-index avoids covering MSW warning banner
@@ -27,14 +31,15 @@ export function ErrorPage({ children }: Props) {
2731
return (
2832
<div className="flex w-full justify-center">
2933
<GradientBackground />
30-
<div className="relative w-full">
34+
<div className="relative flex w-full justify-between">
3135
<Link
3236
to="/"
3337
className="flex items-center p-6 text-mono-sm text-secondary hover:text-default"
3438
>
3539
<PrevArrow12Icon title="Select" className="mr-2 text-tertiary" />
3640
Back to console
3741
</Link>
42+
<SignOutButton className="mr-6 mt-4" />
3843
</div>
3944
<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">
4045
<div className="my-2 flex h-12 w-12 items-center justify-center">
@@ -58,3 +63,19 @@ export function NotFound() {
5863
</ErrorPage>
5964
)
6065
}
66+
67+
export function SignOutButton({ className }: { className?: string }) {
68+
const logout = useApiMutation('logout', {
69+
onSuccess: () => navToLogin({ includeCurrent: false }),
70+
})
71+
return (
72+
<Button
73+
onClick={() => logout.mutate({})}
74+
className={className}
75+
size="sm"
76+
variant="ghost"
77+
>
78+
Sign out
79+
</Button>
80+
)
81+
}

app/components/TopBar.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ import { pb } from '~/util/path-builder'
1919
export function TopBar({ children }: { children: React.ReactNode }) {
2020
const navigate = useNavigate()
2121
const logout = useApiMutation('logout', {
22-
onSuccess: () => {
23-
// server will respond to /login with a login redirect
24-
// TODO-usability: do we just want to dump them back to login or is there
25-
// another page that would make sense, like a logged out homepage
26-
navToLogin({ includeCurrent: false })
27-
},
22+
onSuccess: () => navToLogin({ includeCurrent: false }),
2823
})
2924
// fetch happens in loader wrapping all authed pages
3025
const { me } = useCurrentUser()

mock-api/msw/handlers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
currentUser,
3434
errIfExists,
3535
errIfInvalidDiskSize,
36+
forbiddenErr,
3637
getStartAndEndTime,
3738
getTimestamps,
3839
handleMetrics,
@@ -51,6 +52,7 @@ import {
5152
// is *JSON type.
5253

5354
export const handlers = makeHandlers({
55+
logout: () => 204,
5456
ping: () => ({ status: 'ok' }),
5557
deviceAuthRequest: () => 200,
5658
deviceAuthConfirm: ({ body }) => (body.user_code === 'ERRO-RABC' ? 400 : 200),
@@ -74,7 +76,9 @@ export const handlers = makeHandlers({
7476
},
7577
projectView: ({ path }) => {
7678
if (path.project.endsWith('error-503')) {
77-
throw unavailableErr
79+
throw unavailableErr()
80+
} else if (path.project.endsWith('error-403')) {
81+
throw forbiddenErr()
7882
}
7983

8084
return lookup.project({ ...path })
@@ -1263,7 +1267,6 @@ export const handlers = makeHandlers({
12631267
localIdpUserDelete: NotImplemented,
12641268
localIdpUserSetPassword: NotImplemented,
12651269
loginSaml: NotImplemented,
1266-
logout: NotImplemented,
12671270
networkingAddressLotBlockList: NotImplemented,
12681271
networkingAddressLotCreate: NotImplemented,
12691272
networkingAddressLotDelete: NotImplemented,

mock-api/msw/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export function getTimestamps() {
9292
return { time_created: now, time_modified: now }
9393
}
9494

95+
export const forbiddenErr = () =>
96+
json({ error_code: 'Forbidden', request_id: 'fake-id' }, { status: 403 })
97+
9598
export const unavailableErr = () =>
9699
json({ error_code: 'ServiceUnavailable', request_id: 'fake-id' }, { status: 503 })
97100

test/e2e/error-pages.e2e.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ test('Shows 404 page when a resource is not found', async ({ page }) => {
1313

1414
await page.goto('/projects/nonexistent')
1515
await expect(page.locator('text=Page not found')).toBeVisible()
16+
17+
await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible()
1618
})
1719

1820
test('Shows something went wrong page on other errors', async ({ page }) => {
@@ -31,4 +33,12 @@ test('Shows something went wrong page on other errors', async ({ page }) => {
3133
const error =
3234
'Invariant failed: Expected query to be prefetched. Key: ["projectView",{"path":{"project":"error-503"}}]'
3335
expect(errors.some((e) => e.message.includes(error))).toBeTruthy()
36+
37+
// test clicking sign out
38+
await page.getByRole('button', { name: 'Sign out' }).click()
39+
// login route doesn't actually work in the mock setup (in production this
40+
// is handled by nexus, and it's hard to get Vite to do the right thing here
41+
// without getting elaborate with middleware), so this is a 404, but we do end
42+
// up at the right URL
43+
await expect(page).toHaveURL('/login')
3444
})

0 commit comments

Comments
 (0)