From 749fe1380f1627cc5848f83e1ff16fbca5396950 Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:42:33 +0000 Subject: [PATCH 1/4] fix: handle FailedPrecondition in shadow links route loader --- frontend/src/routes/shadowlinks/index.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/shadowlinks/index.tsx b/frontend/src/routes/shadowlinks/index.tsx index a71168475..a1b29f69f 100644 --- a/frontend/src/routes/shadowlinks/index.tsx +++ b/frontend/src/routes/shadowlinks/index.tsx @@ -10,23 +10,30 @@ */ import { create } from '@bufbuild/protobuf'; +import { Code, ConnectError } from '@connectrpc/connect'; import { createQueryOptions } from '@connectrpc/connect-query'; import { createFileRoute } from '@tanstack/react-router'; import { ShieldIcon } from 'components/icons'; +import { ShadowLinkListPage } from 'components/pages/shadowlinks/list/shadowlink-list-page'; import { ListShadowLinksRequestSchema } from 'protogen/redpanda/api/console/v1alpha1/shadowlink_pb'; import { listShadowLinks } from 'protogen/redpanda/api/console/v1alpha1/shadowlink-ShadowLinkService_connectquery'; -import { ShadowLinkListPage } from '../../components/pages/shadowlinks/list/shadowlink-list-page'; - export const Route = createFileRoute('/shadowlinks/')({ staticData: { title: 'Shadow Links', icon: ShieldIcon, }, loader: async ({ context: { queryClient, dataplaneTransport } }) => { - await queryClient.ensureQueryData( - createQueryOptions(listShadowLinks, create(ListShadowLinksRequestSchema, {}), { transport: dataplaneTransport }) - ); + try { + await queryClient.ensureQueryData( + createQueryOptions(listShadowLinks, create(ListShadowLinksRequestSchema, {}), { transport: dataplaneTransport }) + ); + } catch (error) { + if (error instanceof ConnectError && error.code === Code.FailedPrecondition) { + return; + } + throw error; + } }, component: ShadowLinkListPage, }); From 5e784e1f47ae5c2a5cbe38c285e686b4257dceb6 Mon Sep 17 00:00:00 2001 From: Martin Schneppenheim <23424570+weeco@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:25:36 +0000 Subject: [PATCH 2/4] 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. --- frontend/src/state/supported-features.ts | 2 +- frontend/src/utils/route-utils.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/state/supported-features.ts b/frontend/src/state/supported-features.ts index fb58796c2..f14c67e80 100644 --- a/frontend/src/state/supported-features.ts +++ b/frontend/src/state/supported-features.ts @@ -118,7 +118,7 @@ export function isSupported(f: FeatureEntry): boolean { /** * A list of features we should hide instead of showing a disabled message. */ -const HIDE_IF_NOT_SUPPORTED_FEATURES = [Feature.GetQuotas, Feature.ShadowLinkService, Feature.TracingService]; +const HIDE_IF_NOT_SUPPORTED_FEATURES = [Feature.GetQuotas, Feature.TracingService]; export function shouldHideIfNotSupported(f: FeatureEntry): boolean { return HIDE_IF_NOT_SUPPORTED_FEATURES.includes(f); } diff --git a/frontend/src/utils/route-utils.tsx b/frontend/src/utils/route-utils.tsx index 97b5c911e..76cb43e13 100644 --- a/frontend/src/utils/route-utils.tsx +++ b/frontend/src/utils/route-utils.tsx @@ -290,7 +290,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ if (isEmbedded()) { return isFeatureFlagEnabled('shadowlinkCloudUi') && !isServerless(); } - return true; // self-hosted always visible + return true; }), }, { From 5b33afb9b731901a1c8a9d7ae8b24e244be9c2ac Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:26:33 +0000 Subject: [PATCH 3/4] fix: hide shadow links tab on non-Redpanda clusters --- frontend/src/utils/route-utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/route-utils.tsx b/frontend/src/utils/route-utils.tsx index 76cb43e13..c75340c22 100644 --- a/frontend/src/utils/route-utils.tsx +++ b/frontend/src/utils/route-utils.tsx @@ -290,7 +290,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ if (isEmbedded()) { return isFeatureFlagEnabled('shadowlinkCloudUi') && !isServerless(); } - return true; + return api.isRedpanda; }), }, { From e18768772420796b75ea69265338bc785e25a798 Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:28:35 +0000 Subject: [PATCH 4/4] 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. --- .../list/shadowlink-empty-state.tsx | 20 ++++++++++++++++++- .../shadowlinks/list/shadowlink-list-page.tsx | 14 +++++++++++-- frontend/src/routes/shadowlinks/index.tsx | 5 ++++- frontend/src/utils/route-utils.tsx | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/pages/shadowlinks/list/shadowlink-empty-state.tsx b/frontend/src/components/pages/shadowlinks/list/shadowlink-empty-state.tsx index d22e5f5d7..45fbb631a 100644 --- a/frontend/src/components/pages/shadowlinks/list/shadowlink-empty-state.tsx +++ b/frontend/src/components/pages/shadowlinks/list/shadowlink-empty-state.tsx @@ -9,11 +9,12 @@ * by the Apache License, Version 2.0 */ +import { Alert, AlertDescription } from 'components/redpanda-ui/components/alert'; import { Button } from 'components/redpanda-ui/components/button'; import { Card, CardContent, CardHeader, CardTitle } from 'components/redpanda-ui/components/card'; import { CodeBlock, Pre } from 'components/redpanda-ui/components/code-block'; import { Text } from 'components/redpanda-ui/components/typography'; -import { AlertCircle, SearchX } from 'lucide-react'; +import { AlertCircle, Info, SearchX } from 'lucide-react'; const ShadowingDescription = () => ( <> @@ -86,6 +87,23 @@ export const ShadowLinkFeatureDisabledState = () => ( ); +export const ShadowLinkUnavailableState = () => ( + + + Shadowing + + + + } variant="warning"> + + Shadowing is not available for this cluster. This feature requires a Redpanda cluster with the Admin API + enabled. + + + + +); + type ShadowLinkErrorStateProps = { errorMessage: string; onRetry: () => void; diff --git a/frontend/src/components/pages/shadowlinks/list/shadowlink-list-page.tsx b/frontend/src/components/pages/shadowlinks/list/shadowlink-list-page.tsx index dbdf90fc5..ef4a7a31b 100644 --- a/frontend/src/components/pages/shadowlinks/list/shadowlink-list-page.tsx +++ b/frontend/src/components/pages/shadowlinks/list/shadowlink-list-page.tsx @@ -30,6 +30,7 @@ import { ShadowLinkEmptyStateCloud, ShadowLinkErrorState, ShadowLinkFeatureDisabledState, + ShadowLinkUnavailableState, } from './shadowlink-empty-state'; import { isEmbedded } from '../../../../config'; import { getBasePath } from '../../../../utils/env'; @@ -119,9 +120,9 @@ export const ShadowLinkListPage = () => { uiState.pageTitle = 'Shadow Links'; }, []); - // Show toast on error (except for feature-disabled errors) + // Show toast on error (except for feature-disabled or unavailable admin API errors) useEffect(() => { - if (error && error.code !== Code.FailedPrecondition) { + if (error && error.code !== Code.FailedPrecondition && error.code !== Code.Unavailable) { toast.error('Failed to load shadowlinks', { description: error.message, }); @@ -140,6 +141,15 @@ export const ShadowLinkListPage = () => { getCoreRowModel: getCoreRowModel(), }); + // Admin API unavailable + if (error?.code === Code.Unavailable) { + return ( +
+ +
+ ); + } + // Feature disabled state if (error?.code === Code.FailedPrecondition && error.message.includes('Cluster link feature is disabled')) { return ( diff --git a/frontend/src/routes/shadowlinks/index.tsx b/frontend/src/routes/shadowlinks/index.tsx index a1b29f69f..5a0f513f2 100644 --- a/frontend/src/routes/shadowlinks/index.tsx +++ b/frontend/src/routes/shadowlinks/index.tsx @@ -29,7 +29,10 @@ export const Route = createFileRoute('/shadowlinks/')({ createQueryOptions(listShadowLinks, create(ListShadowLinksRequestSchema, {}), { transport: dataplaneTransport }) ); } catch (error) { - if (error instanceof ConnectError && error.code === Code.FailedPrecondition) { + if ( + error instanceof ConnectError && + (error.code === Code.FailedPrecondition || error.code === Code.Unavailable) + ) { return; } throw error; diff --git a/frontend/src/utils/route-utils.tsx b/frontend/src/utils/route-utils.tsx index c75340c22..76cb43e13 100644 --- a/frontend/src/utils/route-utils.tsx +++ b/frontend/src/utils/route-utils.tsx @@ -290,7 +290,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ if (isEmbedded()) { return isFeatureFlagEnabled('shadowlinkCloudUi') && !isServerless(); } - return api.isRedpanda; + return true; }), }, {