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 a71168475..5a0f513f2 100644
--- a/frontend/src/routes/shadowlinks/index.tsx
+++ b/frontend/src/routes/shadowlinks/index.tsx
@@ -10,23 +10,33 @@
*/
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 || error.code === Code.Unavailable)
+ ) {
+ return;
+ }
+ throw error;
+ }
},
component: ShadowLinkListPage,
});
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;
}),
},
{