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; }), }, {