Skip to content

Commit 0d7d6e6

Browse files
c-julinweeco
andauthored
frontend: fix shadow links page crash when feature is disabled (#2225)
* fix: handle FailedPrecondition in shadow links route loader * fix: remove ShadowLinkService from HIDE_IF_NOT_SUPPORTED_FEATURES Shadow Links should remain visible in the sidebar even when the feature is disabled so users can see instructions on how to enable it. The loader try-catch handles FailedPrecondition gracefully for disabled clusters. * fix: hide shadow links tab on non-Redpanda clusters * fix: show distinct unavailable state for shadow links on non-Redpanda clusters Non-Redpanda clusters (e.g. Apache Kafka) return Code.Unavailable because the Redpanda Admin API doesn't exist. Previously this showed the same FeatureDisabled card with an `rpk` command, which is incorrect for these clusters. Add a separate ShadowLinkUnavailableState with an info alert explaining that shadowing requires a Redpanda cluster with the Admin API. --------- Co-authored-by: Martin Schneppenheim <23424570+weeco@users.noreply.github.com>
1 parent cc3f4bc commit 0d7d6e6

5 files changed

Lines changed: 48 additions & 10 deletions

File tree

frontend/src/components/pages/shadowlinks/list/shadowlink-empty-state.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

12+
import { Alert, AlertDescription } from 'components/redpanda-ui/components/alert';
1213
import { Button } from 'components/redpanda-ui/components/button';
1314
import { Card, CardContent, CardHeader, CardTitle } from 'components/redpanda-ui/components/card';
1415
import { CodeBlock, Pre } from 'components/redpanda-ui/components/code-block';
1516
import { Text } from 'components/redpanda-ui/components/typography';
16-
import { AlertCircle, SearchX } from 'lucide-react';
17+
import { AlertCircle, Info, SearchX } from 'lucide-react';
1718

1819
const ShadowingDescription = () => (
1920
<>
@@ -86,6 +87,23 @@ export const ShadowLinkFeatureDisabledState = () => (
8687
</Card>
8788
);
8889

90+
export const ShadowLinkUnavailableState = () => (
91+
<Card data-testid="shadowlink-unavailable-card" size="full">
92+
<CardHeader>
93+
<CardTitle>Shadowing</CardTitle>
94+
</CardHeader>
95+
<CardContent className="flex flex-col gap-3">
96+
<ShadowingDescription />
97+
<Alert icon={<Info />} variant="warning">
98+
<AlertDescription>
99+
Shadowing is not available for this cluster. This feature requires a Redpanda cluster with the Admin API
100+
enabled.
101+
</AlertDescription>
102+
</Alert>
103+
</CardContent>
104+
</Card>
105+
);
106+
89107
type ShadowLinkErrorStateProps = {
90108
errorMessage: string;
91109
onRetry: () => void;

frontend/src/components/pages/shadowlinks/list/shadowlink-list-page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ShadowLinkEmptyStateCloud,
3131
ShadowLinkErrorState,
3232
ShadowLinkFeatureDisabledState,
33+
ShadowLinkUnavailableState,
3334
} from './shadowlink-empty-state';
3435
import { isEmbedded } from '../../../../config';
3536
import { getBasePath } from '../../../../utils/env';
@@ -119,9 +120,9 @@ export const ShadowLinkListPage = () => {
119120
uiState.pageTitle = 'Shadow Links';
120121
}, []);
121122

122-
// Show toast on error (except for feature-disabled errors)
123+
// Show toast on error (except for feature-disabled or unavailable admin API errors)
123124
useEffect(() => {
124-
if (error && error.code !== Code.FailedPrecondition) {
125+
if (error && error.code !== Code.FailedPrecondition && error.code !== Code.Unavailable) {
125126
toast.error('Failed to load shadowlinks', {
126127
description: error.message,
127128
});
@@ -140,6 +141,15 @@ export const ShadowLinkListPage = () => {
140141
getCoreRowModel: getCoreRowModel(),
141142
});
142143

144+
// Admin API unavailable
145+
if (error?.code === Code.Unavailable) {
146+
return (
147+
<div className="my-2 flex justify-center gap-2">
148+
<ShadowLinkUnavailableState />
149+
</div>
150+
);
151+
}
152+
143153
// Feature disabled state
144154
if (error?.code === Code.FailedPrecondition && error.message.includes('Cluster link feature is disabled')) {
145155
return (

frontend/src/routes/shadowlinks/index.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,33 @@
1010
*/
1111

1212
import { create } from '@bufbuild/protobuf';
13+
import { Code, ConnectError } from '@connectrpc/connect';
1314
import { createQueryOptions } from '@connectrpc/connect-query';
1415
import { createFileRoute } from '@tanstack/react-router';
1516
import { ShieldIcon } from 'components/icons';
17+
import { ShadowLinkListPage } from 'components/pages/shadowlinks/list/shadowlink-list-page';
1618
import { ListShadowLinksRequestSchema } from 'protogen/redpanda/api/console/v1alpha1/shadowlink_pb';
1719
import { listShadowLinks } from 'protogen/redpanda/api/console/v1alpha1/shadowlink-ShadowLinkService_connectquery';
1820

19-
import { ShadowLinkListPage } from '../../components/pages/shadowlinks/list/shadowlink-list-page';
20-
2121
export const Route = createFileRoute('/shadowlinks/')({
2222
staticData: {
2323
title: 'Shadow Links',
2424
icon: ShieldIcon,
2525
},
2626
loader: async ({ context: { queryClient, dataplaneTransport } }) => {
27-
await queryClient.ensureQueryData(
28-
createQueryOptions(listShadowLinks, create(ListShadowLinksRequestSchema, {}), { transport: dataplaneTransport })
29-
);
27+
try {
28+
await queryClient.ensureQueryData(
29+
createQueryOptions(listShadowLinks, create(ListShadowLinksRequestSchema, {}), { transport: dataplaneTransport })
30+
);
31+
} catch (error) {
32+
if (
33+
error instanceof ConnectError &&
34+
(error.code === Code.FailedPrecondition || error.code === Code.Unavailable)
35+
) {
36+
return;
37+
}
38+
throw error;
39+
}
3040
},
3141
component: ShadowLinkListPage,
3242
});

frontend/src/state/supported-features.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function isSupported(f: FeatureEntry): boolean {
118118
/**
119119
* A list of features we should hide instead of showing a disabled message.
120120
*/
121-
const HIDE_IF_NOT_SUPPORTED_FEATURES = [Feature.GetQuotas, Feature.ShadowLinkService, Feature.TracingService];
121+
const HIDE_IF_NOT_SUPPORTED_FEATURES = [Feature.GetQuotas, Feature.TracingService];
122122
export function shouldHideIfNotSupported(f: FeatureEntry): boolean {
123123
return HIDE_IF_NOT_SUPPORTED_FEATURES.includes(f);
124124
}

frontend/src/utils/route-utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
290290
if (isEmbedded()) {
291291
return isFeatureFlagEnabled('shadowlinkCloudUi') && !isServerless();
292292
}
293-
return true; // self-hosted always visible
293+
return true;
294294
}),
295295
},
296296
{

0 commit comments

Comments
 (0)