From 5247889c7822bc3fe0c09e4f4944481f0379f89e Mon Sep 17 00:00:00 2001 From: Anton-Fil Date: Thu, 31 Jul 2025 15:33:51 +0100 Subject: [PATCH 1/3] add view for showing the associated gateways --- console-extensions.json | 15 + package.json | 3 +- src/components/GatewaySingleOverview.tsx | 1 - src/components/HTTPRoute/AttachedGateways.tsx | 318 ++++++++++++++++++ .../HTTPRoute/HTTPRouteSingleOverview.tsx | 55 +++ 5 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 src/components/HTTPRoute/AttachedGateways.tsx create mode 100644 src/components/HTTPRoute/HTTPRouteSingleOverview.tsx diff --git a/console-extensions.json b/console-extensions.json index 649fbe6..7668e19 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -13,5 +13,20 @@ }, "component": { "$codeRef": "GatewaySingleOverview" } } + }, + { + "type": "console.tab/horizontalNav", + "properties": { + "model": { + "group": "gateway.networking.k8s.io", + "version": "v1", + "kind": "HTTPRoute" + }, + "page": { + "name": "Attached", + "href": "attached" + }, + "component": { "$codeRef": "HTTPRouteSingleOverview" } + } } ] diff --git a/package.json b/package.json index 8ad27f7..7852a37 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "loadType": "Preload" }, "exposedModules": { - "GatewaySingleOverview": "./components/GatewaySingleOverview" + "GatewaySingleOverview": "./components/GatewaySingleOverview", + "HTTPRouteSingleOverview": "./components/HTTPRoute/HTTPRouteSingleOverview" }, "dependencies": { "@console/pluginAPI": "*" diff --git a/src/components/GatewaySingleOverview.tsx b/src/components/GatewaySingleOverview.tsx index 7a11330..900b08a 100644 --- a/src/components/GatewaySingleOverview.tsx +++ b/src/components/GatewaySingleOverview.tsx @@ -53,5 +53,4 @@ const GatewayPoliciesPage: React.FC = () => { ); }; - export default GatewayPoliciesPage; diff --git a/src/components/HTTPRoute/AttachedGateways.tsx b/src/components/HTTPRoute/AttachedGateways.tsx new file mode 100644 index 0000000..4aee63e --- /dev/null +++ b/src/components/HTTPRoute/AttachedGateways.tsx @@ -0,0 +1,318 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Alert, + AlertGroup, + EmptyState, + EmptyStateBody, + InputGroup, + MenuToggle, + MenuToggleElement, + Select, + SelectOption, + TextInput, + Title, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import { + K8sResourceKind, + ResourceLink, + useK8sWatchResources, + VirtualizedTable, + TableData, + RowProps, + TableColumn, + WatchK8sResource, +} from '@openshift-console/dynamic-plugin-sdk'; +import { SearchIcon } from '@patternfly/react-icons'; +import { getStatusLabel } from '../../utils/statusLabel'; + +type AttachedGatewaysProps = { + resource: K8sResourceKind; // HTTPRoute +}; + +interface HTTPRoute extends K8sResourceKind { + spec?: { + parentRefs?: Array<{ name: string; namespace?: string; group?: string; kind?: string }>; + }; + status?: { + parents?: Array<{ + parentRef: { name: string; namespace?: string; group?: string; kind?: string }; + conditions?: Array<{ type: string; status: string }>; + }>; + }; +} + +const AttachedGateways: React.FC = ({ resource }) => { + const { t } = useTranslation('plugin__gateway-api-console-plugin'); + const [filters, setFilters] = React.useState(''); + const [isOpen, setIsOpen] = React.useState(false); + const [filterSelected, setFilterSelected] = React.useState('Name'); + const [filteredGateways, setFilteredGateways] = React.useState([]); + // extract Gateway targets from HTTPRoute + const gatewayTargets = React.useMemo(() => { + const httpRoute = resource as HTTPRoute; + + // status.parents > spec.parentRefs + let parentRefs: Array<{ name: string; namespace?: string; group?: string; kind?: string }> = []; + + if (httpRoute.status?.parents) { + parentRefs = httpRoute.status.parents.map((parent) => parent.parentRef); + } else if (httpRoute.spec?.parentRefs) { + parentRefs = httpRoute.spec.parentRefs; + } + + console.log('PARENT REFS FROM HTTPROUTE:', parentRefs); + + // filter only Gateway res + const gatewayRefs = parentRefs.filter((ref) => { + const refKind = ref.kind || 'Gateway'; + const refGroup = ref.group || 'gateway.networking.k8s.io'; + return refKind === 'Gateway' && refGroup === 'gateway.networking.k8s.io'; + }); + + // create unicorn targets + const targets = gatewayRefs.map((ref) => ({ + name: ref.name, + namespace: ref.namespace || httpRoute.metadata.namespace, + })); + + console.log('GATEWAY TARGETS:', targets); + return targets; + }, [resource]); + + // create res for download Gateway + const gatewayResources: { [key: string]: WatchK8sResource } = React.useMemo(() => { + const resources: { [key: string]: WatchK8sResource } = {}; + + gatewayTargets.forEach((target, index) => { + resources[`gateway-${index}`] = { + groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' }, + namespace: target.namespace, + name: target.name, + isList: false, + }; + }); + + console.log('GATEWAY RESOURCES TO WATCH:', resources); + return resources; + }, [gatewayTargets]); + + const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind }>( + gatewayResources, + ); + + // take Gateway + const attachedGateways = React.useMemo(() => { + const gatewaysArray: K8sResourceKind[] = []; + + console.log('WATCHED GATEWAY RESOURCES:', watchedResources); + + Object.entries(watchedResources).forEach(([key, gatewayWatch]) => { + if (gatewayWatch?.loaded && !gatewayWatch.loadError && gatewayWatch.data) { + console.log(`LOADED GATEWAY ${key}:`, gatewayWatch.data); + gatewaysArray.push(gatewayWatch.data as K8sResourceKind); + } else if (gatewayWatch?.loadError) { + console.error(`ERROR LOADING GATEWAY ${key}:`, gatewayWatch.loadError); + } + }); + + console.log('ALL ATTACHED GATEWAYS:', gatewaysArray); + return gatewaysArray; + }, [watchedResources]); + + // Search/filter logic + const onToggleClick = () => setIsOpen(!isOpen); + + const onFilterSelect = ( + _event: React.MouseEvent | undefined, + selection: string, + ) => { + setFilterSelected(selection); + setIsOpen(false); + }; + + const handleFilterChange = (value: string) => { + setFilters(value); + }; + + // Filter attached gateways based on search criteria + React.useEffect(() => { + let data = attachedGateways; + if (filters) { + const filterValue = filters.toLowerCase(); + data = data.filter((gateway) => { + if (filterSelected === 'Name') { + return gateway.metadata.name.toLowerCase().includes(filterValue); + } else if (filterSelected === 'Namespace') { + return gateway.metadata.namespace?.toLowerCase().includes(filterValue); + } else if (filterSelected === 'Gateway Class') { + return gateway.spec?.gatewayClassName?.toLowerCase().includes(filterValue); + } + return true; + }); + } + setFilteredGateways(data); + }, [attachedGateways, filters, filterSelected]); + + const columns: TableColumn[] = [ + { + title: t('plugin__gateway-api-console-plugin~Name'), + id: 'name', + sort: 'metadata.name', + }, + { + title: t('plugin__gateway-api-console-plugin~Namespace'), + id: 'namespace', + sort: 'metadata.namespace', + }, + { + title: t('plugin__gateway-api-console-plugin~Gateway Class'), + id: 'gatewayclass', + }, + { + title: t('plugin__gateway-api-console-plugin~Status'), + id: 'status', + }, + ]; + + const AttachedGatewayRow: React.FC> = ({ obj, activeColumnIDs }) => { + const [group, version] = obj.apiVersion.includes('/') + ? obj.apiVersion.split('/') + : ['gateway.networking.k8s.io', obj.apiVersion]; + + return ( + <> + {columns.map((column) => { + switch (column.id) { + case 'name': + return ( + + + + ); + case 'namespace': + return ( + + {obj.metadata.namespace || '-'} + + ); + case 'gatewayclass': + return ( + + {obj.spec?.gatewayClassName || '-'} + + ); + case 'status': + return ( + + {getStatusLabel(obj)} + + ); + default: + return null; + } + })} + + ); + }; + + const allLoaded = Object.values(watchedResources).every((res) => res.loaded); + const loadErrors = Object.values(watchedResources) + .filter((res) => res.loadError) + .map((res) => res.loadError); + const combinedLoadError = + loadErrors.length > 0 ? new Error(loadErrors.map((err) => err.message).join('; ')) : null; + + return ( +
+ {combinedLoadError && ( + + + {combinedLoadError.message} + + + )} + + {/* Search Toolbar */} + {attachedGateways.length > 0 && ( + + + + + + + + + + handleFilterChange(value)} + className="pf-v5-c-form-control co-text-filter-with-icon" + aria-label="Gateway search" + value={filters} + /> + + + + + + )} + + {filteredGateways.length === 0 && allLoaded ? ( + + {filters ? t('No matching gateways found') : t('No attached gateways found')} + + } + icon={SearchIcon} + > + + {filters + ? t('Try adjusting your search criteria') + : gatewayTargets.length === 0 + ? t('This HTTPRoute has no parentRefs configured') + : t('Referenced gateways could not be loaded')} + + + ) : ( + + data={filteredGateways} + unfilteredData={attachedGateways} + loaded={allLoaded} + loadError={combinedLoadError} + columns={columns} + Row={AttachedGatewayRow} + /> + )} +
+ ); +}; + +export default AttachedGateways; diff --git a/src/components/HTTPRoute/HTTPRouteSingleOverview.tsx b/src/components/HTTPRoute/HTTPRouteSingleOverview.tsx new file mode 100644 index 0000000..0245393 --- /dev/null +++ b/src/components/HTTPRoute/HTTPRouteSingleOverview.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; +import { PageSection, Title } from '@patternfly/react-core'; +import { + useK8sWatchResources, + K8sResourceCommon, + useActiveNamespace, +} from '@openshift-console/dynamic-plugin-sdk'; + +import extractResourceNameFromURL from '../../utils/nameFromPath'; +import { Helmet } from 'react-helmet'; +import AttachedResources from './AttachedGateways'; + +const HTTPRouteSingleOverview: React.FC = () => { + const { t } = useTranslation('plugin__gateway-api-console-plugin'); + const [activeNamespace] = useActiveNamespace(); + const location = useLocation(); + + const httpRouteName = extractResourceNameFromURL(location.pathname); + const resources = { + httpRoute: { + groupVersionKind: { + group: 'gateway.networking.k8s.io', + version: 'v1', + kind: 'HTTPRoute', + }, + namespace: activeNamespace, + name: httpRouteName, + isList: false, + }, + }; + + const watchedResources = useK8sWatchResources<{ httpRoute: K8sResourceCommon }>(resources); + const { loaded, loadError, data: httpRoute } = watchedResources.httpRoute; + + return ( + <> + + {t('Associated Gateways')} + + + {t('Associated Gateways')} + {!loaded ? ( +
Loading...
+ ) : loadError ? ( +
Error loading HTTPRoute: {loadError.message}
+ ) : ( + + )} +
+ + ); +}; +export default HTTPRouteSingleOverview; From 640536b982352ce068033b5ea9b06eb1108e717e Mon Sep 17 00:00:00 2001 From: Anton-Fil Date: Fri, 1 Aug 2025 15:39:08 +0100 Subject: [PATCH 2/3] split httprout and gateway attched --- package.json | 2 +- src/components/AttachedResources.tsx | 467 ++++++++++++++---- src/components/HTTPRoute/AttachedGateways.tsx | 318 ------------ .../HTTPRouteSingleOverview.tsx | 4 +- 4 files changed, 371 insertions(+), 420 deletions(-) delete mode 100644 src/components/HTTPRoute/AttachedGateways.tsx rename src/components/{HTTPRoute => }/HTTPRouteSingleOverview.tsx (93%) diff --git a/package.json b/package.json index 7852a37..bd50f06 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ }, "exposedModules": { "GatewaySingleOverview": "./components/GatewaySingleOverview", - "HTTPRouteSingleOverview": "./components/HTTPRoute/HTTPRouteSingleOverview" + "HTTPRouteSingleOverview": "./components/HTTPRouteSingleOverview" }, "dependencies": { "@console/pluginAPI": "*" diff --git a/src/components/AttachedResources.tsx b/src/components/AttachedResources.tsx index ba68ac5..6f91d45 100644 --- a/src/components/AttachedResources.tsx +++ b/src/components/AttachedResources.tsx @@ -1,6 +1,22 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, AlertGroup, EmptyState, EmptyStateBody, Title } from '@patternfly/react-core'; +import { + Alert, + AlertGroup, + EmptyState, + EmptyStateBody, + InputGroup, + MenuToggle, + MenuToggleElement, + Select, + SelectOption, + TextInput, + Title, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; import { K8sResourceKind, ResourceLink, @@ -18,132 +34,299 @@ type AttachedResourcesProps = { resource: K8sResourceKind; }; +interface HTTPRoute extends K8sResourceKind { + spec?: { + parentRefs?: Array<{ name: string; namespace?: string; group?: string; kind?: string }>; + }; + status?: { + parents?: Array<{ + parentRef: { name: string; namespace?: string; group?: string; kind?: string }; + conditions?: Array<{ type: string; status: string }>; + }>; + }; +} + +// resurs type understanding +const getResourceType = (resource: K8sResourceKind): 'gateway' | 'httproute' | 'unknown' => { + const group = resource.apiVersion.includes('/') ? resource.apiVersion.split('/')[0] : ''; + + if (resource.kind === 'Gateway' && group === 'gateway.networking.k8s.io') { + return 'gateway'; + } + if (resource.kind === 'HTTPRoute' && group === 'gateway.networking.k8s.io') { + return 'httproute'; + } + return 'unknown'; +}; + const AttachedResources: React.FC = ({ resource }) => { const { t } = useTranslation('plugin__gateway-api-console-plugin'); + const [filters, setFilters] = React.useState(''); + const [isOpen, setIsOpen] = React.useState(false); + const [filterSelected, setFilterSelected] = React.useState('Name'); + const [filteredResources, setFilteredResources] = React.useState([]); - const associatedResources: { [key: string]: WatchK8sResource } = { - HTTPRoute: { - groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'HTTPRoute' }, - isList: true, - // Search cluster-wide for HTTPRoutes that might reference this gateway - }, - }; + const resourceType = getResourceType(resource); - const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind[] }>( - associatedResources, - ); + // Gateway -> HTTPRoutes Rach + const useGatewayToHTTPRoutes = () => { + const associatedResources: { [key: string]: WatchK8sResource } = { + HTTPRoute: { + groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'HTTPRoute' }, + isList: true, + }, + }; - const resourceGroup = resource.apiVersion.includes('/') ? resource.apiVersion.split('/')[0] : ''; + const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind[] }>( + associatedResources, + ); - const attachedRoutes = React.useMemo(() => { - let routesArray: K8sResourceKind[] = []; + const resourceGroup = resource.apiVersion.includes('/') + ? resource.apiVersion.split('/')[0] + : ''; - // Process HTTPRoutes - const httpRoutes = watchedResources.HTTPRoute; - console.log('HTTPROUTES', httpRoutes); + const attachedRoutes = React.useMemo(() => { + let routesArray: K8sResourceKind[] = []; - if (httpRoutes?.loaded && !httpRoutes.loadError && httpRoutes.data) { - console.log('ALL HTTP ROUTES:', httpRoutes.data); - const matchingRoutes = (httpRoutes.data as K8sResourceKind[]).filter((route) => { - console.log('CHECKING ROUTE:', route.metadata.name); - console.log('ROUTE STATUS:', route.status); + const httpRoutes = watchedResources.HTTPRoute; + console.log('HTTPROUTES', httpRoutes); - const statusParents = route.status?.parents ?? []; - console.log('STATUS PARENTS FOR ROUTE', route.metadata.name, ':', statusParents); + if (httpRoutes?.loaded && !httpRoutes.loadError && httpRoutes.data) { + console.log('ALL HTTP ROUTES:', httpRoutes.data); + const matchingRoutes = (httpRoutes.data as K8sResourceKind[]).filter((route) => { + console.log('CHECKING ROUTE:', route.metadata.name); + console.log('ROUTE STATUS:', route.status); - // Also check spec.parentRefs as fallback for debugging - const specParents = route.spec?.parentRefs ?? []; - console.log('SPEC PARENTS FOR ROUTE', route.metadata.name, ':', specParents); + const statusParents = route.status?.parents ?? []; + console.log('STATUS PARENTS FOR ROUTE', route.metadata.name, ':', statusParents); - console.log('LOOKING FOR GATEWAY:', { - name: resource.metadata.name, - namespace: resource.metadata.namespace, - kind: resource.kind, - group: resourceGroup, - }); + const specParents = route.spec?.parentRefs ?? []; + console.log('SPEC PARENTS FOR ROUTE', route.metadata.name, ':', specParents); - // Check both status.parents and spec.parentRefs for matches - const checkParentRef = (parentRef: any) => { - if (!parentRef) return false; - - const refNamespace = parentRef.namespace ?? resource.metadata.namespace; - const refGroup = parentRef.group ?? 'gateway.networking.k8s.io'; - const refKind = parentRef.kind ?? 'Gateway'; - - const matches = - parentRef.name === resource.metadata.name && - refNamespace === resource.metadata.namespace && - refGroup === resourceGroup && - refKind === resource.kind; - - console.log('MATCH CHECK:', { - parentRef, - expected: { - name: resource.metadata.name, - namespace: resource.metadata.namespace, - group: resourceGroup, - kind: resource.kind, - }, - actual: { - name: parentRef.name, - namespace: refNamespace, - group: refGroup, - kind: refKind, - }, - matches, + console.log('LOOKING FOR GATEWAY:', { + name: resource.metadata.name, + namespace: resource.metadata.namespace, + kind: resource.kind, + group: resourceGroup, }); - return matches; - }; + const checkParentRef = (parentRef: any) => { + if (!parentRef) return false; - // First check status.parents - const statusMatch = statusParents.some((parent: any) => { - console.log('CHECKING STATUS PARENT REF:', parent.parentRef); - return checkParentRef(parent.parentRef); - }); + const refNamespace = parentRef.namespace ?? resource.metadata.namespace; + const refGroup = parentRef.group ?? 'gateway.networking.k8s.io'; + const refKind = parentRef.kind ?? 'Gateway'; + + const matches = + parentRef.name === resource.metadata.name && + refNamespace === resource.metadata.namespace && + refGroup === resourceGroup && + refKind === resource.kind; + + console.log('MATCH CHECK:', { + parentRef, + expected: { + name: resource.metadata.name, + namespace: resource.metadata.namespace, + group: resourceGroup, + kind: resource.kind, + }, + actual: { + name: parentRef.name, + namespace: refNamespace, + group: refGroup, + kind: refKind, + }, + matches, + }); + + return matches; + }; + + const statusMatch = statusParents.some((parent: any) => { + console.log('CHECKING STATUS PARENT REF:', parent.parentRef); + return checkParentRef(parent.parentRef); + }); + + const specMatch = specParents.some((parentRef: any) => { + console.log('CHECKING SPEC PARENT REF:', parentRef); + return checkParentRef(parentRef); + }); - // Also check spec.parentRefs as fallback - const specMatch = specParents.some((parentRef: any) => { - console.log('CHECKING SPEC PARENT REF:', parentRef); - return checkParentRef(parentRef); + return statusMatch || specMatch; }); + console.log('MATCHING ROUTES:', matchingRoutes); - return statusMatch || specMatch; + routesArray = routesArray.concat(matchingRoutes); + } + + return routesArray; + }, [watchedResources, resource, resourceGroup]); + + return { + attachedResources: attachedRoutes, + watchedResources, + allLoaded: Object.values(watchedResources).every((res) => res.loaded), + }; + }; + + // HTTPRoute -> Gateways Anton + const useHTTPRouteToGateways = () => { + const gatewayTargets = React.useMemo(() => { + const httpRoute = resource as HTTPRoute; + + let parentRefs: Array<{ name: string; namespace?: string; group?: string; kind?: string }> = + []; + + if (httpRoute.status?.parents) { + parentRefs = httpRoute.status.parents.map((parent) => parent.parentRef); + } else if (httpRoute.spec?.parentRefs) { + parentRefs = httpRoute.spec.parentRefs; + } + + console.log('PARENT REFS FROM HTTPROUTE:', parentRefs); + + const gatewayRefs = parentRefs.filter((ref) => { + const refKind = ref.kind || 'Gateway'; + const refGroup = ref.group || 'gateway.networking.k8s.io'; + return refKind === 'Gateway' && refGroup === 'gateway.networking.k8s.io'; }); - console.log('MATCHING ROUTES:', matchingRoutes); - routesArray = routesArray.concat(matchingRoutes); - } + const targets = gatewayRefs.map((ref) => ({ + name: ref.name, + namespace: ref.namespace || httpRoute.metadata.namespace, + })); + + console.log('GATEWAY TARGETS:', targets); + return targets; + }, [resource]); + + const gatewayResources: { [key: string]: WatchK8sResource } = React.useMemo(() => { + const resources: { [key: string]: WatchK8sResource } = {}; + + gatewayTargets.forEach((target, index) => { + resources[`gateway-${index}`] = { + groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' }, + namespace: target.namespace, + name: target.name, + isList: false, + }; + }); + + console.log('GATEWAY RESOURCES TO WATCH:', resources); + return resources; + }, [gatewayTargets]); + + const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind }>( + gatewayResources, + ); + + const attachedGateways = React.useMemo(() => { + const gatewaysArray: K8sResourceKind[] = []; + + console.log('WATCHED GATEWAY RESOURCES:', watchedResources); + + Object.entries(watchedResources).forEach(([key, gatewayWatch]) => { + if (gatewayWatch?.loaded && !gatewayWatch.loadError && gatewayWatch.data) { + console.log(`LOADED GATEWAY ${key}:`, gatewayWatch.data); + gatewaysArray.push(gatewayWatch.data as K8sResourceKind); + } else if (gatewayWatch?.loadError) { + console.error(`ERROR LOADING GATEWAY ${key}:`, gatewayWatch.loadError); + } + }); + + console.log('ALL ATTACHED GATEWAYS:', gatewaysArray); + return gatewaysArray; + }, [watchedResources]); + + return { + attachedResources: attachedGateways, + watchedResources, + allLoaded: Object.values(watchedResources).every((res) => res.loaded), + gatewayTargets, + }; + }; + + // choose type of extraction + let data = null; + + if (resourceType === 'gateway') { + data = useGatewayToHTTPRoutes(); + } else if (resourceType === 'httproute') { + data = useHTTPRouteToGateways(); + } - return routesArray; - }, [watchedResources, resource, resourceGroup]); + let attachedResources: K8sResourceKind[] = []; + let watchedResources: { [key: string]: any } = {}; + let allLoaded = false; + if (data !== null) { + attachedResources = data.attachedResources; + watchedResources = data.watchedResources; + allLoaded = data.allLoaded; + } + + // filtering for all + const onToggleClick = () => setIsOpen(!isOpen); + + const onFilterSelect = ( + _event: React.MouseEvent | undefined, + selection: string, + ) => { + setFilterSelected(selection); + setIsOpen(false); + }; + + const handleFilterChange = (value: string) => { + setFilters(value); + }; + + // filtering + React.useEffect(() => { + let data = attachedResources; + if (filters) { + const filterValue = filters.toLowerCase(); + data = data.filter((obj) => { + if (filterSelected === 'Name') { + return obj.metadata.name.toLowerCase().includes(filterValue); + } else if (filterSelected === 'Namespace') { + return obj.metadata.namespace?.toLowerCase().includes(filterValue); + } else if (filterSelected === 'Kind') { + return obj.kind.toLowerCase().includes(filterValue); + } + return true; + }); + } + setFilteredResources(data); + }, [attachedResources, filters, filterSelected]); + + // Name, Namespace, Kind, Status column const columns: TableColumn[] = [ { title: t('plugin__gateway-api-console-plugin~Name'), id: 'name', sort: 'metadata.name', }, - { - title: t('plugin__gateway-api-console-plugin~Type'), - id: 'type', - sort: 'kind', - }, { title: t('plugin__gateway-api-console-plugin~Namespace'), id: 'namespace', sort: 'metadata.namespace', }, + { + title: t('plugin__gateway-api-console-plugin~Kind'), + id: 'kind', + sort: 'kind', + }, { title: t('plugin__gateway-api-console-plugin~Status'), id: 'status', }, ]; - - const AttachedResourceRow: React.FC> = ({ obj, activeColumnIDs }) => { + const UniversalRow: React.FC> = ({ obj, activeColumnIDs }) => { const [group, version] = obj.apiVersion.includes('/') ? obj.apiVersion.split('/') - : ['', obj.apiVersion]; + : ['gateway.networking.k8s.io', obj.apiVersion]; + return ( <> {columns.map((column) => { @@ -158,16 +341,16 @@ const AttachedResources: React.FC = ({ resource }) => { /> ); - case 'type': + case 'namespace': return ( - {obj.kind} + {obj.metadata.namespace || '-'} ); - case 'namespace': + case 'kind': return ( - {obj.metadata.namespace || '-'} + {obj.kind} ); case 'status': @@ -184,41 +367,127 @@ const AttachedResources: React.FC = ({ resource }) => { ); }; - const allLoaded = Object.values(watchedResources).every((res) => res.loaded); const loadErrors = Object.values(watchedResources) .filter((res) => res.loadError) .map((res) => res.loadError); const combinedLoadError = loadErrors.length > 0 ? new Error(loadErrors.map((err) => err.message).join('; ')) : null; + const displayData = filteredResources; + + const getEmptyStateMessages = () => { + if (resourceType === 'gateway') { + return { + title: t('No attached routes found'), + body: t('No route resources matched'), + }; + } else if (resourceType === 'httproute') { + return { + title: filters ? t('No matching gateways found') : t('No attached gateways found'), + body: filters + ? t('Try adjusting your search criteria') + : data?.gatewayTargets?.length === 0 + ? t('This HTTPRoute has no parentRefs configured') + : t('Referenced gateways could not be loaded'), + }; + } + return { + title: t('Unsupported resource type'), + body: t('This component only supports Gateway and HTTPRoute resources'), + }; + }; + + const emptyStateMessages = getEmptyStateMessages(); + + if (resourceType === 'unknown') { + return ( + + {emptyStateMessages.title} + + } + icon={SearchIcon} + > + {emptyStateMessages.body} + + ); + } return (
{combinedLoadError && ( - + {combinedLoadError.message} )} - {attachedRoutes.length === 0 && allLoaded ? ( + + {attachedResources.length > 0 && ( + + + + + + + + + + handleFilterChange(value)} + className="pf-v5-c-form-control co-text-filter-with-icon" + aria-label="Resource search" + value={filters} + /> + + + + + + )} + + {displayData.length === 0 && allLoaded ? ( - {t('No attached routes found')} + {emptyStateMessages.title} } icon={SearchIcon} > - {t('No route resources matched')} + {emptyStateMessages.body} ) : ( - data={attachedRoutes} - unfilteredData={attachedRoutes} + data={displayData} + unfilteredData={attachedResources} loaded={allLoaded} loadError={combinedLoadError} columns={columns} - Row={AttachedResourceRow} + Row={UniversalRow} /> )}
diff --git a/src/components/HTTPRoute/AttachedGateways.tsx b/src/components/HTTPRoute/AttachedGateways.tsx deleted file mode 100644 index 4aee63e..0000000 --- a/src/components/HTTPRoute/AttachedGateways.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Alert, - AlertGroup, - EmptyState, - EmptyStateBody, - InputGroup, - MenuToggle, - MenuToggleElement, - Select, - SelectOption, - TextInput, - Title, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, -} from '@patternfly/react-core'; -import { - K8sResourceKind, - ResourceLink, - useK8sWatchResources, - VirtualizedTable, - TableData, - RowProps, - TableColumn, - WatchK8sResource, -} from '@openshift-console/dynamic-plugin-sdk'; -import { SearchIcon } from '@patternfly/react-icons'; -import { getStatusLabel } from '../../utils/statusLabel'; - -type AttachedGatewaysProps = { - resource: K8sResourceKind; // HTTPRoute -}; - -interface HTTPRoute extends K8sResourceKind { - spec?: { - parentRefs?: Array<{ name: string; namespace?: string; group?: string; kind?: string }>; - }; - status?: { - parents?: Array<{ - parentRef: { name: string; namespace?: string; group?: string; kind?: string }; - conditions?: Array<{ type: string; status: string }>; - }>; - }; -} - -const AttachedGateways: React.FC = ({ resource }) => { - const { t } = useTranslation('plugin__gateway-api-console-plugin'); - const [filters, setFilters] = React.useState(''); - const [isOpen, setIsOpen] = React.useState(false); - const [filterSelected, setFilterSelected] = React.useState('Name'); - const [filteredGateways, setFilteredGateways] = React.useState([]); - // extract Gateway targets from HTTPRoute - const gatewayTargets = React.useMemo(() => { - const httpRoute = resource as HTTPRoute; - - // status.parents > spec.parentRefs - let parentRefs: Array<{ name: string; namespace?: string; group?: string; kind?: string }> = []; - - if (httpRoute.status?.parents) { - parentRefs = httpRoute.status.parents.map((parent) => parent.parentRef); - } else if (httpRoute.spec?.parentRefs) { - parentRefs = httpRoute.spec.parentRefs; - } - - console.log('PARENT REFS FROM HTTPROUTE:', parentRefs); - - // filter only Gateway res - const gatewayRefs = parentRefs.filter((ref) => { - const refKind = ref.kind || 'Gateway'; - const refGroup = ref.group || 'gateway.networking.k8s.io'; - return refKind === 'Gateway' && refGroup === 'gateway.networking.k8s.io'; - }); - - // create unicorn targets - const targets = gatewayRefs.map((ref) => ({ - name: ref.name, - namespace: ref.namespace || httpRoute.metadata.namespace, - })); - - console.log('GATEWAY TARGETS:', targets); - return targets; - }, [resource]); - - // create res for download Gateway - const gatewayResources: { [key: string]: WatchK8sResource } = React.useMemo(() => { - const resources: { [key: string]: WatchK8sResource } = {}; - - gatewayTargets.forEach((target, index) => { - resources[`gateway-${index}`] = { - groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' }, - namespace: target.namespace, - name: target.name, - isList: false, - }; - }); - - console.log('GATEWAY RESOURCES TO WATCH:', resources); - return resources; - }, [gatewayTargets]); - - const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind }>( - gatewayResources, - ); - - // take Gateway - const attachedGateways = React.useMemo(() => { - const gatewaysArray: K8sResourceKind[] = []; - - console.log('WATCHED GATEWAY RESOURCES:', watchedResources); - - Object.entries(watchedResources).forEach(([key, gatewayWatch]) => { - if (gatewayWatch?.loaded && !gatewayWatch.loadError && gatewayWatch.data) { - console.log(`LOADED GATEWAY ${key}:`, gatewayWatch.data); - gatewaysArray.push(gatewayWatch.data as K8sResourceKind); - } else if (gatewayWatch?.loadError) { - console.error(`ERROR LOADING GATEWAY ${key}:`, gatewayWatch.loadError); - } - }); - - console.log('ALL ATTACHED GATEWAYS:', gatewaysArray); - return gatewaysArray; - }, [watchedResources]); - - // Search/filter logic - const onToggleClick = () => setIsOpen(!isOpen); - - const onFilterSelect = ( - _event: React.MouseEvent | undefined, - selection: string, - ) => { - setFilterSelected(selection); - setIsOpen(false); - }; - - const handleFilterChange = (value: string) => { - setFilters(value); - }; - - // Filter attached gateways based on search criteria - React.useEffect(() => { - let data = attachedGateways; - if (filters) { - const filterValue = filters.toLowerCase(); - data = data.filter((gateway) => { - if (filterSelected === 'Name') { - return gateway.metadata.name.toLowerCase().includes(filterValue); - } else if (filterSelected === 'Namespace') { - return gateway.metadata.namespace?.toLowerCase().includes(filterValue); - } else if (filterSelected === 'Gateway Class') { - return gateway.spec?.gatewayClassName?.toLowerCase().includes(filterValue); - } - return true; - }); - } - setFilteredGateways(data); - }, [attachedGateways, filters, filterSelected]); - - const columns: TableColumn[] = [ - { - title: t('plugin__gateway-api-console-plugin~Name'), - id: 'name', - sort: 'metadata.name', - }, - { - title: t('plugin__gateway-api-console-plugin~Namespace'), - id: 'namespace', - sort: 'metadata.namespace', - }, - { - title: t('plugin__gateway-api-console-plugin~Gateway Class'), - id: 'gatewayclass', - }, - { - title: t('plugin__gateway-api-console-plugin~Status'), - id: 'status', - }, - ]; - - const AttachedGatewayRow: React.FC> = ({ obj, activeColumnIDs }) => { - const [group, version] = obj.apiVersion.includes('/') - ? obj.apiVersion.split('/') - : ['gateway.networking.k8s.io', obj.apiVersion]; - - return ( - <> - {columns.map((column) => { - switch (column.id) { - case 'name': - return ( - - - - ); - case 'namespace': - return ( - - {obj.metadata.namespace || '-'} - - ); - case 'gatewayclass': - return ( - - {obj.spec?.gatewayClassName || '-'} - - ); - case 'status': - return ( - - {getStatusLabel(obj)} - - ); - default: - return null; - } - })} - - ); - }; - - const allLoaded = Object.values(watchedResources).every((res) => res.loaded); - const loadErrors = Object.values(watchedResources) - .filter((res) => res.loadError) - .map((res) => res.loadError); - const combinedLoadError = - loadErrors.length > 0 ? new Error(loadErrors.map((err) => err.message).join('; ')) : null; - - return ( -
- {combinedLoadError && ( - - - {combinedLoadError.message} - - - )} - - {/* Search Toolbar */} - {attachedGateways.length > 0 && ( - - - - - - - - - - handleFilterChange(value)} - className="pf-v5-c-form-control co-text-filter-with-icon" - aria-label="Gateway search" - value={filters} - /> - - - - - - )} - - {filteredGateways.length === 0 && allLoaded ? ( - - {filters ? t('No matching gateways found') : t('No attached gateways found')} - - } - icon={SearchIcon} - > - - {filters - ? t('Try adjusting your search criteria') - : gatewayTargets.length === 0 - ? t('This HTTPRoute has no parentRefs configured') - : t('Referenced gateways could not be loaded')} - - - ) : ( - - data={filteredGateways} - unfilteredData={attachedGateways} - loaded={allLoaded} - loadError={combinedLoadError} - columns={columns} - Row={AttachedGatewayRow} - /> - )} -
- ); -}; - -export default AttachedGateways; diff --git a/src/components/HTTPRoute/HTTPRouteSingleOverview.tsx b/src/components/HTTPRouteSingleOverview.tsx similarity index 93% rename from src/components/HTTPRoute/HTTPRouteSingleOverview.tsx rename to src/components/HTTPRouteSingleOverview.tsx index 0245393..9cd31d4 100644 --- a/src/components/HTTPRoute/HTTPRouteSingleOverview.tsx +++ b/src/components/HTTPRouteSingleOverview.tsx @@ -8,9 +8,9 @@ import { useActiveNamespace, } from '@openshift-console/dynamic-plugin-sdk'; -import extractResourceNameFromURL from '../../utils/nameFromPath'; +import extractResourceNameFromURL from '../utils/nameFromPath'; import { Helmet } from 'react-helmet'; -import AttachedResources from './AttachedGateways'; +import AttachedResources from './AttachedResources'; const HTTPRouteSingleOverview: React.FC = () => { const { t } = useTranslation('plugin__gateway-api-console-plugin'); From c69afd9753c413ed2bd2c9e2cd3f2a07f582d737 Mon Sep 17 00:00:00 2001 From: Anton-Fil Date: Mon, 11 Aug 2025 15:02:00 +0100 Subject: [PATCH 3/3] attached hateway and httprute view --- src/components/AttachedResources.tsx | 447 +++++---------------------- 1 file changed, 79 insertions(+), 368 deletions(-) diff --git a/src/components/AttachedResources.tsx b/src/components/AttachedResources.tsx index 6f91d45..d16b07a 100644 --- a/src/components/AttachedResources.tsx +++ b/src/components/AttachedResources.tsx @@ -1,22 +1,6 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { - Alert, - AlertGroup, - EmptyState, - EmptyStateBody, - InputGroup, - MenuToggle, - MenuToggleElement, - Select, - SelectOption, - TextInput, - Title, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, -} from '@patternfly/react-core'; +import { Alert, AlertGroup, EmptyState, EmptyStateBody, Title } from '@patternfly/react-core'; import { K8sResourceKind, ResourceLink, @@ -34,299 +18,112 @@ type AttachedResourcesProps = { resource: K8sResourceKind; }; -interface HTTPRoute extends K8sResourceKind { - spec?: { - parentRefs?: Array<{ name: string; namespace?: string; group?: string; kind?: string }>; - }; - status?: { - parents?: Array<{ - parentRef: { name: string; namespace?: string; group?: string; kind?: string }; - conditions?: Array<{ type: string; status: string }>; - }>; - }; -} - -// resurs type understanding -const getResourceType = (resource: K8sResourceKind): 'gateway' | 'httproute' | 'unknown' => { - const group = resource.apiVersion.includes('/') ? resource.apiVersion.split('/')[0] : ''; - - if (resource.kind === 'Gateway' && group === 'gateway.networking.k8s.io') { - return 'gateway'; - } - if (resource.kind === 'HTTPRoute' && group === 'gateway.networking.k8s.io') { - return 'httproute'; - } - return 'unknown'; -}; - const AttachedResources: React.FC = ({ resource }) => { const { t } = useTranslation('plugin__gateway-api-console-plugin'); - const [filters, setFilters] = React.useState(''); - const [isOpen, setIsOpen] = React.useState(false); - const [filterSelected, setFilterSelected] = React.useState('Name'); - const [filteredResources, setFilteredResources] = React.useState([]); - const resourceType = getResourceType(resource); + const associatedResources: { [key: string]: WatchK8sResource } = { + HTTPRoute: { + groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'HTTPRoute' }, + isList: true, + }, + Gateway: { + groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' }, + isList: true, + }, + }; - // Gateway -> HTTPRoutes Rach - const useGatewayToHTTPRoutes = () => { - const associatedResources: { [key: string]: WatchK8sResource } = { - HTTPRoute: { - groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'HTTPRoute' }, - isList: true, - }, - }; + const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind[] }>( + associatedResources, + ); - const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind[] }>( - associatedResources, - ); + const resourceGroup = resource.apiVersion.includes('/') ? resource.apiVersion.split('/')[0] : ''; - const resourceGroup = resource.apiVersion.includes('/') - ? resource.apiVersion.split('/')[0] - : ''; + const attachedResources = React.useMemo(() => { + let results: K8sResourceKind[] = []; - const attachedRoutes = React.useMemo(() => { - let routesArray: K8sResourceKind[] = []; + const checkParentRef = (parentRef: any, targetResource: K8sResourceKind) => { + if (!parentRef) return false; - const httpRoutes = watchedResources.HTTPRoute; - console.log('HTTPROUTES', httpRoutes); + const refNamespace = parentRef.namespace ?? targetResource.metadata.namespace; + const refGroup = parentRef.group ?? 'gateway.networking.k8s.io'; + const refKind = parentRef.kind ?? 'Gateway'; - if (httpRoutes?.loaded && !httpRoutes.loadError && httpRoutes.data) { - console.log('ALL HTTP ROUTES:', httpRoutes.data); - const matchingRoutes = (httpRoutes.data as K8sResourceKind[]).filter((route) => { - console.log('CHECKING ROUTE:', route.metadata.name); - console.log('ROUTE STATUS:', route.status); + return ( + parentRef.name === targetResource.metadata.name && + refNamespace === targetResource.metadata.namespace && + refGroup === resourceGroup && + refKind === targetResource.kind + ); + }; + if (resource.kind === 'Gateway') { + const httpRoutes = watchedResources.HTTPRoute; + if (httpRoutes?.loaded && !httpRoutes.loadError && httpRoutes.data) { + const matchingRoutes = httpRoutes.data.filter((route) => { const statusParents = route.status?.parents ?? []; - console.log('STATUS PARENTS FOR ROUTE', route.metadata.name, ':', statusParents); - const specParents = route.spec?.parentRefs ?? []; - console.log('SPEC PARENTS FOR ROUTE', route.metadata.name, ':', specParents); - - console.log('LOOKING FOR GATEWAY:', { - name: resource.metadata.name, - namespace: resource.metadata.namespace, - kind: resource.kind, - group: resourceGroup, - }); - - const checkParentRef = (parentRef: any) => { - if (!parentRef) return false; - - const refNamespace = parentRef.namespace ?? resource.metadata.namespace; - const refGroup = parentRef.group ?? 'gateway.networking.k8s.io'; - const refKind = parentRef.kind ?? 'Gateway'; - - const matches = - parentRef.name === resource.metadata.name && - refNamespace === resource.metadata.namespace && - refGroup === resourceGroup && - refKind === resource.kind; - console.log('MATCH CHECK:', { - parentRef, - expected: { - name: resource.metadata.name, - namespace: resource.metadata.namespace, - group: resourceGroup, - kind: resource.kind, - }, - actual: { - name: parentRef.name, - namespace: refNamespace, - group: refGroup, - kind: refKind, - }, - matches, - }); - - return matches; - }; - - const statusMatch = statusParents.some((parent: any) => { - console.log('CHECKING STATUS PARENT REF:', parent.parentRef); - return checkParentRef(parent.parentRef); - }); - - const specMatch = specParents.some((parentRef: any) => { - console.log('CHECKING SPEC PARENT REF:', parentRef); - return checkParentRef(parentRef); - }); + const statusMatch = statusParents.some((parent: any) => + checkParentRef(parent.parentRef, resource), + ); + const specMatch = specParents.some((parentRef: any) => + checkParentRef(parentRef, resource), + ); return statusMatch || specMatch; }); - console.log('MATCHING ROUTES:', matchingRoutes); - - routesArray = routesArray.concat(matchingRoutes); + results = results.concat(matchingRoutes); } + } - return routesArray; - }, [watchedResources, resource, resourceGroup]); - - return { - attachedResources: attachedRoutes, - watchedResources, - allLoaded: Object.values(watchedResources).every((res) => res.loaded), - }; - }; - - // HTTPRoute -> Gateways Anton - const useHTTPRouteToGateways = () => { - const gatewayTargets = React.useMemo(() => { - const httpRoute = resource as HTTPRoute; - - let parentRefs: Array<{ name: string; namespace?: string; group?: string; kind?: string }> = - []; - - if (httpRoute.status?.parents) { - parentRefs = httpRoute.status.parents.map((parent) => parent.parentRef); - } else if (httpRoute.spec?.parentRefs) { - parentRefs = httpRoute.spec.parentRefs; + if (resource.kind === 'HTTPRoute') { + const gateways = watchedResources.Gateway; + if (gateways?.loaded && !gateways.loadError && gateways.data) { + const matchingGateways = gateways.data.filter((gateway) => { + const specParents = resource.spec?.parentRefs ?? []; + + return specParents.some((parentRef: any) => { + return ( + parentRef.name === gateway.metadata.name && + (parentRef.namespace ?? resource.metadata.namespace) === gateway.metadata.namespace && + (parentRef.group ?? 'gateway.networking.k8s.io') === resourceGroup && + (parentRef.kind ?? 'Gateway') === gateway.kind + ); + }); + }); + results = results.concat(matchingGateways); } - - console.log('PARENT REFS FROM HTTPROUTE:', parentRefs); - - const gatewayRefs = parentRefs.filter((ref) => { - const refKind = ref.kind || 'Gateway'; - const refGroup = ref.group || 'gateway.networking.k8s.io'; - return refKind === 'Gateway' && refGroup === 'gateway.networking.k8s.io'; - }); - - const targets = gatewayRefs.map((ref) => ({ - name: ref.name, - namespace: ref.namespace || httpRoute.metadata.namespace, - })); - - console.log('GATEWAY TARGETS:', targets); - return targets; - }, [resource]); - - const gatewayResources: { [key: string]: WatchK8sResource } = React.useMemo(() => { - const resources: { [key: string]: WatchK8sResource } = {}; - - gatewayTargets.forEach((target, index) => { - resources[`gateway-${index}`] = { - groupVersionKind: { group: 'gateway.networking.k8s.io', version: 'v1', kind: 'Gateway' }, - namespace: target.namespace, - name: target.name, - isList: false, - }; - }); - - console.log('GATEWAY RESOURCES TO WATCH:', resources); - return resources; - }, [gatewayTargets]); - - const watchedResources = useK8sWatchResources<{ [key: string]: K8sResourceKind }>( - gatewayResources, - ); - - const attachedGateways = React.useMemo(() => { - const gatewaysArray: K8sResourceKind[] = []; - - console.log('WATCHED GATEWAY RESOURCES:', watchedResources); - - Object.entries(watchedResources).forEach(([key, gatewayWatch]) => { - if (gatewayWatch?.loaded && !gatewayWatch.loadError && gatewayWatch.data) { - console.log(`LOADED GATEWAY ${key}:`, gatewayWatch.data); - gatewaysArray.push(gatewayWatch.data as K8sResourceKind); - } else if (gatewayWatch?.loadError) { - console.error(`ERROR LOADING GATEWAY ${key}:`, gatewayWatch.loadError); - } - }); - - console.log('ALL ATTACHED GATEWAYS:', gatewaysArray); - return gatewaysArray; - }, [watchedResources]); - - return { - attachedResources: attachedGateways, - watchedResources, - allLoaded: Object.values(watchedResources).every((res) => res.loaded), - gatewayTargets, - }; - }; - - // choose type of extraction - let data = null; - - if (resourceType === 'gateway') { - data = useGatewayToHTTPRoutes(); - } else if (resourceType === 'httproute') { - data = useHTTPRouteToGateways(); - } - - let attachedResources: K8sResourceKind[] = []; - let watchedResources: { [key: string]: any } = {}; - let allLoaded = false; - - if (data !== null) { - attachedResources = data.attachedResources; - watchedResources = data.watchedResources; - allLoaded = data.allLoaded; - } - - // filtering for all - const onToggleClick = () => setIsOpen(!isOpen); - - const onFilterSelect = ( - _event: React.MouseEvent | undefined, - selection: string, - ) => { - setFilterSelected(selection); - setIsOpen(false); - }; - - const handleFilterChange = (value: string) => { - setFilters(value); - }; - - // filtering - React.useEffect(() => { - let data = attachedResources; - if (filters) { - const filterValue = filters.toLowerCase(); - data = data.filter((obj) => { - if (filterSelected === 'Name') { - return obj.metadata.name.toLowerCase().includes(filterValue); - } else if (filterSelected === 'Namespace') { - return obj.metadata.namespace?.toLowerCase().includes(filterValue); - } else if (filterSelected === 'Kind') { - return obj.kind.toLowerCase().includes(filterValue); - } - return true; - }); } - setFilteredResources(data); - }, [attachedResources, filters, filterSelected]); - // Name, Namespace, Kind, Status column + return results; + }, [watchedResources, resource, resourceGroup]); + const columns: TableColumn[] = [ { title: t('plugin__gateway-api-console-plugin~Name'), id: 'name', sort: 'metadata.name', }, + { + title: t('plugin__gateway-api-console-plugin~Type'), + id: 'type', + sort: 'kind', + }, { title: t('plugin__gateway-api-console-plugin~Namespace'), id: 'namespace', sort: 'metadata.namespace', }, - { - title: t('plugin__gateway-api-console-plugin~Kind'), - id: 'kind', - sort: 'kind', - }, { title: t('plugin__gateway-api-console-plugin~Status'), id: 'status', }, ]; - const UniversalRow: React.FC> = ({ obj, activeColumnIDs }) => { + + const AttachedResourceRow: React.FC> = ({ obj, activeColumnIDs }) => { const [group, version] = obj.apiVersion.includes('/') ? obj.apiVersion.split('/') - : ['gateway.networking.k8s.io', obj.apiVersion]; - + : ['', obj.apiVersion]; return ( <> {columns.map((column) => { @@ -341,16 +138,16 @@ const AttachedResources: React.FC = ({ resource }) => { /> ); - case 'namespace': + case 'type': return ( - {obj.metadata.namespace || '-'} + {obj.kind} ); - case 'kind': + case 'namespace': return ( - {obj.kind} + {obj.metadata.namespace || '-'} ); case 'status': @@ -367,127 +164,41 @@ const AttachedResources: React.FC = ({ resource }) => { ); }; + const allLoaded = Object.values(watchedResources).every((res) => res.loaded); const loadErrors = Object.values(watchedResources) .filter((res) => res.loadError) .map((res) => res.loadError); const combinedLoadError = loadErrors.length > 0 ? new Error(loadErrors.map((err) => err.message).join('; ')) : null; - const displayData = filteredResources; - - const getEmptyStateMessages = () => { - if (resourceType === 'gateway') { - return { - title: t('No attached routes found'), - body: t('No route resources matched'), - }; - } else if (resourceType === 'httproute') { - return { - title: filters ? t('No matching gateways found') : t('No attached gateways found'), - body: filters - ? t('Try adjusting your search criteria') - : data?.gatewayTargets?.length === 0 - ? t('This HTTPRoute has no parentRefs configured') - : t('Referenced gateways could not be loaded'), - }; - } - return { - title: t('Unsupported resource type'), - body: t('This component only supports Gateway and HTTPRoute resources'), - }; - }; - - const emptyStateMessages = getEmptyStateMessages(); - - if (resourceType === 'unknown') { - return ( - - {emptyStateMessages.title} - - } - icon={SearchIcon} - > - {emptyStateMessages.body} - - ); - } return (
{combinedLoadError && ( - + {combinedLoadError.message} )} - - {attachedResources.length > 0 && ( - - - - - - - - - - handleFilterChange(value)} - className="pf-v5-c-form-control co-text-filter-with-icon" - aria-label="Resource search" - value={filters} - /> - - - - - - )} - - {displayData.length === 0 && allLoaded ? ( + {attachedResources.length === 0 && allLoaded ? ( - {emptyStateMessages.title} + {t('No attached resources found')} } icon={SearchIcon} > - {emptyStateMessages.body} + {t('No matching resources found')} ) : ( - data={displayData} + data={attachedResources} unfilteredData={attachedResources} loaded={allLoaded} loadError={combinedLoadError} columns={columns} - Row={UniversalRow} + Row={AttachedResourceRow} /> )}