From ac02795d7e355a56c90f3332ee401bc622a5d3a7 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Tue, 19 May 2026 11:23:40 -0400 Subject: [PATCH] OCPBUGS-71878: Show empty state instead of 403 error for users without projects Resource list pages that use useK8sWatchResource directly were ignoring the mock prop passed by withStartGuide when no projects are available. This caused API calls that returned 403 errors, displayed as "Restricted access" instead of the expected "No {resource} found" empty state. Accept and respect the mock prop in PodsPage, PodDisruptionBudgetsPage, VolumeSnapshotPage, and RoleBindingsPage to skip API calls and forward mock to ConsoleDataView, which already renders an EmptyBox when mock is true. Co-Authored-By: Claude Opus 4.6 --- .../src/components/pdb/PDBList.tsx | 1 + .../src/components/pdb/PDBPage.tsx | 41 ++++++++++++------- .../volume-snapshot/volume-snapshot.tsx | 33 +++++++++------ .../components/list-page/HelmReleaseList.tsx | 7 ++-- .../list-page/HelmReleaseListPage.tsx | 4 +- .../components/list-page/HelmTabbedPage.tsx | 11 +++-- frontend/public/components/RBAC/bindings.tsx | 34 +++++++++------ frontend/public/components/pod-list.tsx | 34 +++++++++------ 8 files changed, 104 insertions(+), 61 deletions(-) diff --git a/frontend/packages/console-app/src/components/pdb/PDBList.tsx b/frontend/packages/console-app/src/components/pdb/PDBList.tsx index 4c5c69d3aff..8855b7feea4 100644 --- a/frontend/packages/console-app/src/components/pdb/PDBList.tsx +++ b/frontend/packages/console-app/src/components/pdb/PDBList.tsx @@ -180,4 +180,5 @@ type PodDisruptionBudgetsListProps = { data: PodDisruptionBudgetKind[]; loaded: boolean; loadError?: any; + mock?: boolean; }; diff --git a/frontend/packages/console-app/src/components/pdb/PDBPage.tsx b/frontend/packages/console-app/src/components/pdb/PDBPage.tsx index 4c3835188f4..cd8f7f0e54c 100644 --- a/frontend/packages/console-app/src/components/pdb/PDBPage.tsx +++ b/frontend/packages/console-app/src/components/pdb/PDBPage.tsx @@ -10,20 +10,25 @@ import PodDisruptionBudgetList from './PDBList'; import type { PodDisruptionBudgetKind } from './types'; export const PodDisruptionBudgetsPage: FC = ({ + mock, namespace, showTitle = true, }) => { const { t } = useTranslation(); - const [resources, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { - group: PodDisruptionBudgetModel.apiGroup, - kind: PodDisruptionBudgetModel.kind, - version: PodDisruptionBudgetModel.apiVersion, - }, - isList: true, - namespaced: true, - namespace, - }); + const [resources, loaded, loadError] = useK8sWatchResource( + mock + ? null + : { + groupVersionKind: { + group: PodDisruptionBudgetModel.apiGroup, + kind: PodDisruptionBudgetModel.kind, + version: PodDisruptionBudgetModel.apiVersion, + }, + isList: true, + namespaced: true, + namespace, + }, + ); const resourceKind = referenceForModel(PodDisruptionBudgetModel); const accessReview = { @@ -33,18 +38,26 @@ export const PodDisruptionBudgetsPage: FC = ({ return ( <> - - {t('console-app~Create PodDisruptionBudget')} - + {!mock && ( + + {t('console-app~Create PodDisruptionBudget')} + + )} - + ); }; type PodDisruptionBudgetsPageProps = { + mock?: boolean; namespace: string; showTitle?: boolean; }; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx index a068b97b061..e5ff9132199 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx @@ -310,6 +310,7 @@ const VolumeSnapshotTable: FC = ({ data, loaded, ...pr export const VolumeSnapshotPage: FC = ({ canCreate = true, + mock, showTitle = true, namespace, selector, @@ -318,29 +319,33 @@ export const VolumeSnapshotPage: FC = ({ const createPath = `/k8s/ns/${namespace || 'default'}/${VolumeSnapshotModel.plural}/~new/form`; - const [resources, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { - group: VolumeSnapshotModel.apiGroup, - kind: VolumeSnapshotModel.kind, - version: VolumeSnapshotModel.apiVersion, - }, - isList: true, - namespaced: true, - namespace, - selector, - }); + const [resources, loaded, loadError] = useK8sWatchResource( + mock + ? null + : { + groupVersionKind: { + group: VolumeSnapshotModel.apiGroup, + kind: VolumeSnapshotModel.kind, + version: VolumeSnapshotModel.apiVersion, + }, + isList: true, + namespaced: true, + namespace, + selector, + }, + ); return ( <> - {canCreate && ( + {canCreate && !mock && ( {t('console-app~Create VolumeSnapshot')} )} - + ); @@ -387,6 +392,7 @@ type VolumeSnapshotFilters = ResourceFilters & { }; type VolumeSnapshotPageProps = { + mock?: boolean; namespace?: string; canCreate?: boolean; showTitle?: boolean; @@ -402,6 +408,7 @@ type VolumeSnapshotTableProps = { data: VolumeSnapshotKind[]; loaded: boolean; loadError: unknown; + mock?: boolean; }; type VolumeSnapshotRowData = { diff --git a/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseList.tsx b/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseList.tsx index 2d75a916d8f..00b14ca544e 100644 --- a/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseList.tsx +++ b/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseList.tsx @@ -139,7 +139,7 @@ const getObjectMetadata = (release: HelmRelease) => ({ name: release.name, }); -const HelmReleaseList: FC = () => { +const HelmReleaseList: FC<{ mock?: boolean }> = ({ mock }) => { const { t } = useTranslation(); const params = useParams(); const namespace = params.ns; @@ -163,7 +163,7 @@ const HelmReleaseList: FC = () => { [namespace], ); const [secretsData, secretsLoaded, secretsLoadError] = useK8sWatchResource( - secretResource, + mock ? null : secretResource, ); const newCount = secretsData?.length ?? 0; @@ -274,7 +274,7 @@ const HelmReleaseList: FC = () => { {t('helm-plugin~Helm Releases')} }> - {hasNoReleases ? ( + {!mock && hasNoReleases ? ( emptyState() ) : ( @@ -292,6 +292,7 @@ const HelmReleaseList: FC = () => { hideColumnManagement isResizable resetAllColumnWidths={resetAllColumnWidths} + mock={mock} /> )} diff --git a/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseListPage.tsx b/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseListPage.tsx index 461b9d7306d..a0f8acbd521 100644 --- a/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseListPage.tsx +++ b/frontend/packages/helm-plugin/src/components/list-page/HelmReleaseListPage.tsx @@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; import HelmReleaseList from './HelmReleaseList'; -const HelmReleaseListPage: FC = () => { +const HelmReleaseListPage: FC<{ mock?: boolean }> = ({ mock }) => { const { t } = useTranslation(); return (
- +
); }; diff --git a/frontend/packages/helm-plugin/src/components/list-page/HelmTabbedPage.tsx b/frontend/packages/helm-plugin/src/components/list-page/HelmTabbedPage.tsx index 3a4845aeb9a..a4d31d6486f 100644 --- a/frontend/packages/helm-plugin/src/components/list-page/HelmTabbedPage.tsx +++ b/frontend/packages/helm-plugin/src/components/list-page/HelmTabbedPage.tsx @@ -19,7 +19,7 @@ import HelmReleaseList from './HelmReleaseList'; import HelmReleaseListPage from './HelmReleaseListPage'; import RepositoriesPage from './RepositoriesListPage'; -const HelmPage: FC<{ namespace: string | undefined }> = ({ namespace }) => { +const HelmPage: FC<{ mock?: boolean; namespace: string | undefined }> = ({ mock, namespace }) => { const { t } = useTranslation(); const isHelmVisible = useFlag('HELM_CHARTS_CATALOG_TYPE'); const [showTitle, canCreate] = [false, false]; @@ -85,6 +85,9 @@ const HelmPage: FC<{ namespace: string | undefined }> = ({ namespace }) => { // t('helm-plugin~Helm Releases') nameKey: 'helm-plugin~Helm Releases', component: HelmReleaseList, + pageData: { + mock, + }, }, { href: 'repositories', @@ -120,17 +123,17 @@ const HelmPage: FC<{ namespace: string | undefined }> = ({ namespace }) => { telemetryPrefix="Helm" /> ) : ( - + ); }; -export const PageContents: FC = () => { +export const PageContents: FC<{ noProjectsAvailable?: boolean }> = ({ noProjectsAvailable }) => { const { t } = useTranslation(); const { ns: namespace } = useParams(); const [activePerspective] = useActivePerspective(); return activePerspective === 'admin' || namespace ? ( - + ) : ( {(openProjectModal) => ( diff --git a/frontend/public/components/RBAC/bindings.tsx b/frontend/public/components/RBAC/bindings.tsx index eb33cf76e08..1539add89c7 100644 --- a/frontend/public/components/RBAC/bindings.tsx +++ b/frontend/public/components/RBAC/bindings.tsx @@ -361,19 +361,26 @@ export const RoleBindingsPage: FC = ({ }`, }) => { const { t } = useTranslation(); - const resources = useK8sWatchResources({ - RoleBinding: { - kind: 'RoleBinding', - namespaced: true, - namespace, - isList: true, - }, - ClusterRoleBinding: { - kind: 'ClusterRoleBinding', - namespaced: false, - isList: true, - }, - }); + const watchResources = useMemo( + () => + mock + ? {} + : { + RoleBinding: { + kind: 'RoleBinding', + namespaced: true, + namespace, + isList: true, + }, + ClusterRoleBinding: { + kind: 'ClusterRoleBinding', + namespaced: false, + isList: true, + }, + }, + [mock, namespace], + ); + const resources = useK8sWatchResources(watchResources); // Only flatten when at least one resource has data to prevent undefined iteration const data = useMemo(() => { @@ -406,6 +413,7 @@ export const RoleBindingsPage: FC = ({ data={data} loaded={loaded} loadError={loadError} + mock={mock} staticFilters={staticFilters} /> diff --git a/frontend/public/components/pod-list.tsx b/frontend/public/components/pod-list.tsx index df2a54de7d8..d881153a458 100644 --- a/frontend/public/components/pod-list.tsx +++ b/frontend/public/components/pod-list.tsx @@ -454,6 +454,7 @@ export const PodList: FC = ({ data, loaded, loadError, + mock, hideNameLabelFilters, hideLabelFilter, hideColumnManagement, @@ -558,6 +559,7 @@ export const PodList: FC = ({ data={data} loaded={loaded} loadError={loadError} + mock={mock} columns={columns} columnLayout={columnLayout} columnManagementID={columnManagementID} @@ -580,6 +582,7 @@ export const PodList: FC = ({ export const PodsPage: FC = ({ canCreate = true, namespace, + mock, showNodes, showTitle = true, selector, @@ -598,7 +601,7 @@ export const PodsPage: FC = ({ ); useEffect(() => { - if (showMetrics) { + if (showMetrics && !mock) { const updateMetrics = () => fetchPodMetrics(namespace || '') .then((result) => dispatch(UIActions.setPodMetrics(result))) @@ -615,16 +618,20 @@ export const PodsPage: FC = ({ return () => clearInterval(id); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [namespace]); - - const [pods, loaded, loadError] = useK8sWatchResource({ - kind: PodModel.kind, - isList: true, - namespaced: true, - namespace, - selector, - fieldSelector, - }); + }, [mock, namespace]); + + const [pods, loaded, loadError] = useK8sWatchResource( + mock + ? null + : { + kind: PodModel.kind, + isList: true, + namespaced: true, + namespace, + selector, + fieldSelector, + }, + ); const resourceKind = referenceForModel(PodModel); const accessReview = { @@ -639,7 +646,7 @@ export const PodsPage: FC = ({ return ( <> - {canCreate && ( + {canCreate && !mock && ( {t('public~Create Pod')} @@ -650,6 +657,7 @@ export const PodsPage: FC = ({ data={pods} loaded={loaded} loadError={loadError} + mock={mock} showNamespaceOverride={showNamespaceOverride} showNodes={showNodes} namespace={namespace} @@ -691,6 +699,7 @@ type PodListProps = { data: PodKind[]; loaded: boolean; loadError: unknown; + mock?: boolean; showNodes?: boolean; showNamespaceOverride?: boolean; hideNameLabelFilters?: boolean; @@ -703,6 +712,7 @@ type PodListProps = { type PodPageProps = { canCreate?: boolean; fieldSelector?: string; + mock?: boolean; namespace?: string; selector?: Selector; showTitle?: boolean;