Skip to content

Commit 0657bba

Browse files
committed
[cache components] persist cache bypass UI until it's disabled
1 parent f50c769 commit 0657bba

File tree

4 files changed

+131
-17
lines changed

4 files changed

+131
-17
lines changed

packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/next-logo.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ export function NextLogo({
5050
state.buildingIndicator ||
5151
state.renderingIndicator ||
5252
isCacheFilling ||
53-
(isCacheBypassing && state.renderingIndicator)
53+
isCacheBypassing
5454

5555
// Delay showing for 400ms to catch fast operations,
5656
// and keep visible for minimum time (longer for warnings)
5757
const { rendered: showStatusIndicator } = useDelayedRender(shouldShowStatus, {
58-
enterDelay: isCacheBypassing && state.renderingIndicator ? 0 : 400, // Bypass warning shows immediately, others delayed
58+
enterDelay: isCacheBypassing ? 0 : 400, // Bypass warning shows immediately, others delayed
5959
exitDelay: 500,
6060
})
6161

@@ -175,7 +175,8 @@ export function NextLogo({
175175
}
176176
}
177177
178-
&[data-status='cache-bypassing'] {
178+
&[data-status='cache-bypassing']:not([data-error='true']),
179+
&[data-cache-bypassing='true']:not([data-error='true']) {
179180
background: rgba(251, 211, 141, 0.95); /* Warm amber background */
180181
--color-inner-border: rgba(245, 158, 11, 0.8);
181182
@@ -379,6 +380,7 @@ export function NextLogo({
379380
data-error={hasError}
380381
data-error-expanded={isExpanded}
381382
data-status={hasError ? Status.None : currentStatus}
383+
data-cache-bypassing={isCacheBypassing}
382384
data-animate={newErrorDetected}
383385
style={{ width }}
384386
>
@@ -476,7 +478,10 @@ export function NextLogo({
476478
{showStatusIndicator &&
477479
!hasError &&
478480
!state.disableDevIndicator && (
479-
<StatusIndicator status={displayStatus} />
481+
<StatusIndicator
482+
status={displayStatus}
483+
onClick={onTriggerClick}
484+
/>
480485
)}
481486
</>
482487
)}

packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/status-indicator.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ export function getCurrentStatus(
1717
const isCacheFilling = cacheIndicator === 'filling'
1818
const isCacheBypassing = cacheIndicator === 'bypass'
1919

20-
// Priority order: cache bypassing > prerendering > compiling > rendering
21-
if (isCacheBypassing && renderingIndicator) {
20+
// Priority order: compiling > cache bypassing > prerendering > rendering
21+
if (buildingIndicator) {
22+
return Status.Compiling
23+
}
24+
if (isCacheBypassing) {
2225
return Status.CacheBypassing
2326
}
2427
if (isCacheFilling) {
2528
return Status.Prerendering
2629
}
27-
if (buildingIndicator) {
28-
return Status.Compiling
29-
}
3030
if (renderingIndicator) {
3131
return Status.Rendering
3232
}
@@ -35,9 +35,10 @@ export function getCurrentStatus(
3535

3636
interface StatusIndicatorProps {
3737
status: Status
38+
onClick?: () => void
3839
}
3940

40-
export function StatusIndicator({ status }: StatusIndicatorProps) {
41+
export function StatusIndicator({ status, onClick }: StatusIndicatorProps) {
4142
const statusText: Record<Status, string> = {
4243
[Status.None]: '',
4344
[Status.CacheBypassing]: 'Cache disabled',
@@ -78,6 +79,15 @@ export function StatusIndicator({ status }: StatusIndicatorProps) {
7879
font-size: var(--size-13);
7980
font-weight: 500;
8081
white-space: nowrap;
82+
border: none;
83+
background: transparent;
84+
cursor: pointer;
85+
outline: none;
86+
}
87+
88+
[data-indicator-status]:focus-visible {
89+
outline: 2px solid var(--color-blue-800, #3b82f6);
90+
outline-offset: 3px;
8191
}
8292
8393
[data-status-dot] {
@@ -149,7 +159,11 @@ export function StatusIndicator({ status }: StatusIndicatorProps) {
149159
}
150160
`}
151161
</style>
152-
<div data-indicator-status>
162+
<button
163+
data-indicator-status
164+
onClick={onClick}
165+
aria-label="Open Next.js Dev Tools"
166+
>
153167
{statusDotColor[status] && (
154168
<div
155169
data-status-dot
@@ -161,29 +175,34 @@ export function StatusIndicator({ status }: StatusIndicatorProps) {
161175
<AnimateStatusText
162176
key={status} // Key here triggers re-mount and animation
163177
statusKey={status}
178+
showEllipsis={status !== Status.CacheBypassing}
164179
>
165180
{statusText[status]}
166181
</AnimateStatusText>
167-
</div>
182+
</button>
168183
</>
169184
)
170185
}
171186

172187
function AnimateStatusText({
173188
children: text,
189+
showEllipsis = true,
174190
}: {
175191
children: string
176192
statusKey?: string // Keep for type compatibility but unused
193+
showEllipsis?: boolean
177194
}) {
178195
return (
179196
<div data-status-text-animation>
180197
<div data-status-text-enter>
181198
{text}
182-
<span data-status-ellipsis>
183-
<span>.</span>
184-
<span>.</span>
185-
<span>.</span>
186-
</span>
199+
{showEllipsis && (
200+
<span data-status-ellipsis>
201+
<span>.</span>
202+
<span>.</span>
203+
<span>.</span>
204+
</span>
205+
)}
187206
</div>
188207
</div>
189208
)

packages/next/src/server/app-render/app-render.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,10 @@ async function renderToStream(
24772477
if (isBypassingCachesInDev(renderOpts, requestStore)) {
24782478
// Mark the RSC payload to indicate that caches were bypassed in dev.
24792479
// This lets the client know not to cache anything based on this render.
2480+
if (renderOpts.setCacheStatus) {
2481+
// we know this is available when cacheComponents is enabled, but typeguard to be safe
2482+
renderOpts.setCacheStatus('bypass', htmlRequestId, requestId)
2483+
}
24802484
payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, {
24812485
route: workStore.route,
24822486
})

test/development/app-dir/cache-indicator/cache-indicator.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,90 @@ describe('cache-indicator', () => {
4040
expect(status).toBe('none')
4141
})
4242
}
43+
44+
it('shows cache-bypassing indicator when cache is disabled', async () => {
45+
const browser = await next.browser('/', {
46+
extraHTTPHeaders: { 'cache-control': 'no-cache' },
47+
})
48+
49+
// Wait for the badge to appear and show cache-bypassing status
50+
await retry(async () => {
51+
const badge = await browser.elementByCss('[data-next-badge]')
52+
const cacheBypassingAttr = await badge.getAttribute(
53+
'data-cache-bypassing'
54+
)
55+
expect(cacheBypassingAttr).toBe('true')
56+
})
57+
58+
// Verify the status indicator shows cache-bypassing
59+
await retry(async () => {
60+
const badge = await browser.elementByCss('[data-next-badge]')
61+
const status = await badge.getAttribute('data-status')
62+
expect(status).toBe('cache-bypassing')
63+
})
64+
})
65+
66+
it('persists cache-bypassing indicator after navigation when cache is disabled', async () => {
67+
const browser = await next.browser('/', {
68+
extraHTTPHeaders: { 'cache-control': 'no-cache' },
69+
})
70+
71+
// Wait for initial cache-bypassing indicator
72+
await retry(async () => {
73+
const badge = await browser.elementByCss('[data-next-badge]')
74+
const cacheBypassingAttr = await badge.getAttribute(
75+
'data-cache-bypassing'
76+
)
77+
expect(cacheBypassingAttr).toBe('true')
78+
})
79+
80+
// Navigate to another page
81+
const link = await browser.waitForElementByCss('a[href="/navigation"]')
82+
await link.click()
83+
84+
// Wait for navigation to complete
85+
await retry(async () => {
86+
const text = await browser.elementByCss('#navigation-page').text()
87+
expect(text).toContain('Hello navigation page!')
88+
})
89+
90+
// Verify cache-bypassing indicator persists
91+
await retry(async () => {
92+
const badge = await browser.elementByCss('[data-next-badge]')
93+
const cacheBypassingAttr = await badge.getAttribute(
94+
'data-cache-bypassing'
95+
)
96+
expect(cacheBypassingAttr).toBe('true')
97+
})
98+
99+
// Verify status shows cache-bypassing (not rendering)
100+
const badge = await browser.elementByCss('[data-next-badge]')
101+
const status = await badge.getAttribute('data-status')
102+
expect(status).toBe('cache-bypassing')
103+
})
104+
105+
it('opens devtools menu when clicking cache-bypassing indicator', async () => {
106+
const browser = await next.browser('/', {
107+
extraHTTPHeaders: { 'cache-control': 'no-cache' },
108+
})
109+
110+
// Wait for the cache-bypassing indicator to appear
111+
await retry(async () => {
112+
const badge = await browser.elementByCss('[data-next-badge]')
113+
const status = await badge.getAttribute('data-status')
114+
expect(status).toBe('cache-bypassing')
115+
})
116+
117+
// Click the status indicator
118+
const statusIndicator = await browser.elementByCss(
119+
'[data-indicator-status]'
120+
)
121+
await statusIndicator.click()
122+
123+
// Verify devtools menu opens
124+
await retry(async () => {
125+
const hasMenu = await browser.hasElementByCss('#nextjs-dev-tools-menu')
126+
expect(hasMenu).toBe(true)
127+
})
128+
})
43129
})

0 commit comments

Comments
 (0)