diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index b47216de..18f0369c 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -7,61 +7,53 @@ on: workflow_dispatch: env: - IMAGE_BASE_NAME: kuadrant/console-plugin - REGISTRY: quay.io + IMG_REGISTRY_HOST: quay.io + IMG_REGISTRY_ORG: kuadrant + IMG_REGISTRY_REPO: console-plugin jobs: build: name: Build and Push Multi-Arch Image - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - arch: [amd64, arm64] - + runs-on: ubuntu-latest + outputs: + image: ${{ steps.meta.outputs.tags }} steps: - name: Checkout Code - uses: actions/checkout@v4 - - - name: Install QEMU - run: | - sudo apt-get update - sudo apt-get install -y qemu-user-static - - - name: Build Image - id: build - uses: redhat-actions/buildah-build@v2 + uses: actions/checkout@v5 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to container registry + uses: docker/login-action@v2 with: - image: ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }} - tags: latest-${{ matrix.arch }} - archs: ${{ matrix.arch }} - containerfiles: | - ./Dockerfile - - - name: Push Architecture-Specific Image - uses: redhat-actions/push-to-registry@v2 + username: ${{ secrets.IMG_REGISTRY_USERNAME }} + password: ${{ secrets.IMG_REGISTRY_TOKEN }} + registry: ${{ env.IMG_REGISTRY_HOST }} + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.QUAY_USER }} - password: ${{ secrets.QUAY_PASSWORD }} - image: ${{ env.IMAGE_BASE_NAME }} - tags: latest-${{ matrix.arch }} - - manifest: - name: Create and Push Multi-Arch Manifest - needs: build - runs-on: ubuntu-22.04 - steps: - - name: Install Buildah - run: sudo apt-get update && sudo apt-get install -y buildah - - - name: Create Manifest - run: | - buildah manifest create ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest - buildah manifest add ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest-amd64 - buildah manifest add ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest-arm64 - - - name: Push Multi-Arch Manifest + images: ${{ env.IMG_REGISTRY_HOST }}/${{ env.IMG_REGISTRY_ORG }}/${{ env.IMG_REGISTRY_REPO }} + flavor: | + latest=false + tags: | + # git sha + type=raw,value=${{ github.sha }},enable={{is_default_branch}} + # set latest tag for default branch + type=raw,value=latest,enable={{is_default_branch}} + - name: Build and Push Image + if: github.repository_owner == 'kuadrant' + id: build-image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64,linux/s390x + push: true + provenance: false + tags: ${{ steps.meta.outputs.tags }} + - name: Print Image URL run: | - buildah manifest push --all ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:latest \ - --creds=${{ secrets.QUAY_USER }}:${{ secrets.QUAY_PASSWORD }} + echo "Image(s) pushed:" + echo "${{ steps.meta.outputs.tags }}" diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index 78afa7d0..a8b1a728 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - arch: [amd64, arm64] + arch: [amd64, arm64, s390x] steps: - name: Checkout Code @@ -60,6 +60,7 @@ jobs: buildah manifest create ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }} buildah manifest add ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }} docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }}-amd64 buildah manifest add ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }} docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }}-arm64 + buildah manifest add ${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }} docker://${{ env.REGISTRY }}/${{ env.IMAGE_BASE_NAME }}:${{ github.event.release.name }}-s390x - name: Push Multi-Arch Release Manifest run: | diff --git a/CLAUDE.md b/CLAUDE.md index 9b29f264..509eae26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,6 +91,22 @@ const accessReviews = useAccessReviews(resourceAttributes); const canRead = accessReviews[0]; ``` +### 5. Configuration + +The plugin supports configurable Topology andPrometheus metrics for gateway traffic monitoring. This allows the console to work with different Gateway API implementations (OpenShift 4.19+, OSSM, etc.). + +**Configuration is managed through:** +- `src/utils/configLoader.ts` - Configuration schema and defaults +- `src/utils/metricsQueries.ts` - Query utilities +- Environment variables in deployment manifests + +**Example ENV Configuration:** +```yaml +TOPOLOGY_CONFIGMAP_NAME: "topology" +TOPOLOGY_CONFIGMAP_NAMESPACE: "kuadrant-system" +METRICS_WORKLOAD_SUFFIX: "-openshift-default" +``` + ## Key Components - **KuadrantOverviewPage**: Main dashboard with gateway health status diff --git a/charts/openshift-console-plugin/templates/deployment.yaml b/charts/openshift-console-plugin/templates/deployment.yaml index 4c9c74c9..46b3a47e 100644 --- a/charts/openshift-console-plugin/templates/deployment.yaml +++ b/charts/openshift-console-plugin/templates/deployment.yaml @@ -23,6 +23,13 @@ spec: - containerPort: {{ .Values.plugin.port }} protocol: TCP imagePullPolicy: {{ .Values.plugin.imagePullPolicy }} + env: + - name: TOPOLOGY_CONFIGMAP_NAME + value: {{ .Values.plugin.topologyConfigMapName | default "topology" | quote }} + - name: TOPOLOGY_CONFIGMAP_NAMESPACE + value: {{ .Values.plugin.topologyConfigMapNamespace | default "kuadrant-system" | quote }} + - name: METRICS_WORKLOAD_SUFFIX + value: {{ .Values.plugin.metricsWorkloadSuffix | default "-openshift-default" | quote }} {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.containerSecurityContext) }} securityContext: {{ tpl (toYaml (omit .Values.plugin.containerSecurityContext "enabled")) $ | nindent 12 }} {{- end }} diff --git a/charts/openshift-console-plugin/values.yaml b/charts/openshift-console-plugin/values.yaml index 6cc865ba..47bcb8a5 100644 --- a/charts/openshift-console-plugin/values.yaml +++ b/charts/openshift-console-plugin/values.yaml @@ -33,6 +33,9 @@ plugin: create: true annotations: {} name: "" + topologyConfigMapName: "topology" + topologyConfigMapNamespace: "kuadrant-system" + metricsWorkloadSuffix: "-openshift-default" jobs: patchConsoles: enabled: true diff --git a/console-extensions.json b/console-extensions.json index 8077ae06..54bfb154 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -37,6 +37,14 @@ "component": { "$codeRef": "KuadrantOverviewPage" } } }, + { + "type": "console.page/route", + "properties": { + "exact": true, + "path": "/kuadrant/ns/:ns/overview", + "component": { "$codeRef": "KuadrantOverviewPage" } + } + }, { "type": "console.page/route", "properties": { diff --git a/docs/designs/2026-03-27-namespace-scoped-overview-design.md b/docs/designs/2026-03-27-namespace-scoped-overview-design.md new file mode 100644 index 00000000..f77a16d2 --- /dev/null +++ b/docs/designs/2026-03-27-namespace-scoped-overview-design.md @@ -0,0 +1,438 @@ +# Namespace-Scoped Overview Page + +**Date:** 2026-03-27 +**Issue:** [#298](https://github.com/Kuadrant/kuadrant-console-plugin/issues/298) +**Status:** Approved + +## Problem Statement + +The Overview page (`/kuadrant/overview`) was built with cluster-admin users in mind. It watches all resources cluster-wide using `#ALL_NS#`, which breaks for namespace-scoped users who lack cluster-level read permissions. These users see incomplete or missing data, especially in the gateway health cards. + +**Current Issues:** +- Gateway health card watches gateways cluster-wide and shows no results for namespace-scoped users +- All ResourceList components hardcoded to `#ALL_NS#` +- RBAC checks incorrectly resolve to 'default' namespace even when viewing cluster-wide + +## Solution Overview + +Add namespace support to the Overview page using OpenShift's `NamespaceBar` component and URL-based routing, consistent with the existing Policies page pattern. The page will intelligently default to the appropriate view based on user permissions. + +## Design + +### Route Structure + +Add two routes in `console-extensions.json`: + +1. `/kuadrant/overview` - Cluster-wide view (stays here if user has cluster-wide permissions) +2. `/kuadrant/ns/:ns/overview` - Namespace-scoped view (redirects here if user lacks cluster-wide permissions) + +### Smart Default Behavior + +When a user first lands on `/kuadrant/overview`, the component will: + +1. Perform RBAC check for cluster-wide Gateway list permission: + ```typescript + const clusterWideAllowed = await checkAccess({ + group: 'gateway.networking.k8s.io', + resource: 'gateways', + verb: 'list' + // no namespace parameter = cluster-wide check + }); + ``` + +2. Redirect based on permission: + - If `allowed === true` → stay on `/kuadrant/overview` (cluster-wide view) + - If `allowed === false` → redirect to `/kuadrant/ns/default/overview` (fallback namespace) + +3. After initial redirect, respect URL parameter (no further redirects) +4. Users can switch to their accessible namespace via NamespaceBar dropdown + +### UI Components + +**NamespaceBar Integration:** +- Use OpenShift's `` component (from `@openshift-console/dynamic-plugin-sdk`) +- Automatically filters namespaces based on user RBAC +- Shows "All namespaces" option only if user has cluster-wide permissions +- Handles namespace search and filtering + +**Page Layout:** +```text +┌──────────────────────────────────────────────────────┐ +│ [NamespaceBar - OpenShift built-in component] │ +├──────────────────────────────────────────────────────┤ +│ Kuadrant Overview │ +│ │ +│ [Kuadrant CR Status Alert] │ +│ [Getting Started Card - static, always visible] │ +│ │ +│ [Gateway Health Summary - respects namespace] │ +│ [Gateway Traffic Table - respects namespace] │ +│ [Policies Card - respects namespace] │ +│ [HTTPRoutes Card - respects namespace] │ +└──────────────────────────────────────────────────────┘ +``` + +### Data Flow + +```text +1. User navigates to /kuadrant/overview + ↓ +2. Check cluster-wide Gateway list permission + ↓ +3. Stay on /kuadrant/overview (if allowed) + OR redirect to /kuadrant/ns/default/overview (if denied) + ↓ +4. NamespaceBar component displays, filtered by user's accessible namespaces + ↓ +5. Watch resources scoped to namespace from URL (ns param or activeNamespace) + ↓ +6. User changes namespace via NamespaceBar + ↓ +7. handleNamespaceChange updates URL to /kuadrant/ns/{namespace}/overview + ↓ +8. Component re-renders with new namespace, re-watches resources +``` + +### Implementation Details + +#### KuadrantOverviewPage.tsx Changes + +**1. Import NamespaceBar:** +```typescript +import { NamespaceBar } from '@openshift-console/dynamic-plugin-sdk'; +``` + +**2. Read namespace from URL:** +```typescript +const { ns } = useParams<{ ns: string }>(); +const navigate = useNavigate(); +const location = useLocation(); +``` + +**3. Smart default redirect:** +```typescript +React.useEffect(() => { + const performRedirect = async () => { + if (location.pathname === '/kuadrant/overview') { + try { + const result = await checkAccess({ + group: 'gateway.networking.k8s.io', + resource: 'gateways', + verb: 'list' + }); + + // If user doesn't have cluster-wide access, redirect to namespace-scoped view + if (!result.status?.allowed) { + const targetNamespace = + activeNamespace && activeNamespace !== '#ALL_NS#' ? activeNamespace : 'default'; + navigate(`/kuadrant/ns/${targetNamespace}/overview`, { replace: true }); + } + // Otherwise, stay on /kuadrant/overview (cluster-wide view) + } catch (error) { + // On error, redirect to namespace-scoped view + const targetNamespace = + activeNamespace && activeNamespace !== '#ALL_NS#' ? activeNamespace : 'default'; + navigate(`/kuadrant/ns/${targetNamespace}/overview`, { replace: true }); + } + } + }; + + performRedirect(); +}, [location.pathname, activeNamespace, navigate]); +``` + +**4. Determine namespace for watches:** +```typescript +// Read namespace from URL param or fall back to activeNamespace +const watchNamespace = ns || activeNamespace; +``` + +**5. Update resource watches:** + +Replace all hardcoded `namespace="#ALL_NS#"` with `namespace={watchNamespace}`: + +- Gateway watch - **Critical:** Use conditional to handle `#ALL_NS#` + ```typescript + const [gateways] = useK8sWatchResource({ + groupVersionKind: gvk, + isList: true, + namespace: watchNamespace === '#ALL_NS#' ? undefined : watchNamespace, + }); + ``` +- ResourceList for Gateways Traffic (line 910) - uses `namespace={watchNamespace}` +- ResourceList for Policies (line 1019) - uses `namespace={watchNamespace}` +- ResourceList for HTTPRoutes (line 1059) - uses `namespace={watchNamespace}` + +**6. Add Prometheus metrics filtering:** + +The gateway traffic metrics must also respect namespace selection. Use utility functions from `src/utils/metricsQueries.ts`: + +```typescript +// Determine namespace for metrics filtering (undefined for cluster-wide) +const metricsNamespace = watchNamespace === '#ALL_NS#' ? undefined : watchNamespace; + +// Build queries as memoized values to ensure proper re-fetching when namespace changes +const totalRequestsQuery = React.useMemo( + () => buildTotalRequestsQuery(metricsNamespace), + [metricsNamespace], +); + +const totalErrorsQuery = React.useMemo( + () => buildErrorRequestQuery(metricsNamespace), + [metricsNamespace], +); + +const totalErrorsByCodeQuery = React.useMemo( + () => buildErrorsByCodeQuery(metricsNamespace), + [metricsNamespace], +); +``` + +**Metrics query utilities** (in `src/utils/metricsQueries.ts`): +```typescript +const escapePromQLLabelValue = (value: string): string => + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + +const buildNamespaceFilter = (namespace?: string): string => + namespace ? `source_workload_namespace="${escapePromQLLabelValue(namespace)}"` : ''; + +export const buildTotalRequestsQuery = (namespace?: string): string => { + const filter = buildNamespaceFilter(namespace); + return filter + ? `sum by (source_workload, source_workload_namespace) (increase(istio_requests_total{${filter}}[24h]))` + : 'sum by (source_workload, source_workload_namespace) (increase(istio_requests_total[24h]))'; +}; +``` + +**Why memoization is critical:** +- `usePrometheusPoll` hook doesn't automatically re-poll when query string changes +- `React.useMemo` ensures new query object triggers re-execution +- Without memoization, switching namespaces won't update metrics + +**7. Fix RBAC checks:** + +Current (broken): +```typescript +const resolvedNamespace = activeNamespace === '#ALL_NS#' ? 'default' : activeNamespace; +``` + +New (correct): +```typescript +const resolvedNamespace = watchNamespace === '#ALL_NS#' ? undefined : watchNamespace; +``` + +**Why this matters:** +- `undefined` performs cluster-wide RBAC check +- `'default'` would only check permissions in the default namespace +- This was a critical bug fix + +**8. Add NamespaceBar with callback:** +```typescript +const handleNamespaceChange = (newNamespace: string) => { + if (newNamespace !== '#ALL_NS#') { + navigate(`/kuadrant/ns/${newNamespace}/overview`, { replace: true }); + } else { + navigate('/kuadrant/overview', { replace: true }); + } +}; + +return ( + <> + + + {/* existing content */} + + +); +``` + +#### console-extensions.json Changes + +**Add new routes:** +```json +{ + "type": "console.page/route", + "properties": { + "exact": true, + "path": "/kuadrant/overview", + "component": { "$codeRef": "KuadrantOverviewPage" } + } +}, +{ + "type": "console.page/route", + "properties": { + "exact": true, + "path": "/kuadrant/ns/:ns/overview", + "component": { "$codeRef": "KuadrantOverviewPage" } + } +} +``` + +**Navigation links remain unchanged:** +```json +{ + "type": "console.navigation/href", + "properties": { + "id": "kuadrant-overview-admin", + "name": "%plugin__kuadrant-console-plugin~Overview%", + "href": "/kuadrant/overview", + "perspective": "admin", + "section": "kuadrant-section-admin" + } +}, +{ + "type": "console.navigation/href", + "properties": { + "id": "kuadrant-dashboard-dev", + "name": "%plugin__kuadrant-console-plugin~Overview%", + "href": "/kuadrant/overview", + "perspective": "dev", + "section": "kuadrant-section-dev" + } +} +``` + +**Note:** Navigation links point to `/kuadrant/overview` which will redirect namespace-scoped users automatically. This is simpler than the original three-route design. + +## User Experience + +### Cluster-Admin User + +1. Clicks "Overview" in navigation +2. Stays on `/kuadrant/overview` +3. Sees NamespaceBar with "All namespaces" selected +4. Sees cluster-wide gateway health, policies, routes, and traffic metrics +5. Can switch to specific namespace via NamespaceBar (URL updates to `/kuadrant/ns/{namespace}/overview`) + +### Namespace-Scoped User (Single Namespace) + +1. Clicks "Overview" in navigation +2. Redirects to `/kuadrant/ns/default/overview` (fallback namespace) +3. Sees "Access Denied" messages (no permissions in default namespace) +4. Uses NamespaceBar dropdown to switch to their accessible namespace (e.g., `kuadrant-test`) +5. URL updates to `/kuadrant/ns/kuadrant-test/overview` +6. Sees namespace-scoped data (gateways, policies, routes, and traffic metrics) +7. Cannot select "All namespaces" (option not shown due to lack of cluster-wide permissions) + +### Namespace-Scoped User (Multi-Namespace) + +1. Clicks "Overview" in navigation +2. Redirects to `/kuadrant/ns/default/overview` (fallback namespace) or `/kuadrant/ns/{activeNamespace}/overview` if already scoped +3. Sees NamespaceBar with accessible namespaces +4. Can switch between accessible namespaces via dropdown +5. Cannot select "All namespaces" (option not shown due to lack of cluster-wide permissions) + +### Direct URL Access + +Users can bookmark and share namespace-specific views: +- `/kuadrant/overview` - cluster-wide view (if permitted, otherwise redirects) +- `/kuadrant/ns/production/overview` - production namespace view +- `/kuadrant/ns/staging/overview` - staging namespace view + +## Backward Compatibility + +**Navigation Links:** +- Navigation links remain at `/kuadrant/overview` (smart redirect handles routing) +- Users with cluster-wide permissions stay on `/kuadrant/overview` (cluster-wide view) +- Namespace-scoped users get redirected to `/kuadrant/ns/default/overview`, then can switch to accessible namespace + +**No Breaking Changes:** +- Cluster-admins see identical data by default (cluster-wide view on `/kuadrant/overview`) +- All existing functionality preserved +- No state/database migrations required +- Simpler routing structure than originally planned (2 routes instead of 3) + +## Testing Scenarios + +1. **Cluster-admin navigates to overview:** + - Stays on `/kuadrant/overview` + - NamespaceBar shows "All namespaces" + all accessible namespaces + - All cards show cluster-wide data + +2. **Namespace-scoped user navigates to overview:** + - Redirects to `/kuadrant/ns/default/overview` (fallback) + - NamespaceBar shows only accessible namespaces + - User sees "Access Denied" initially (no permissions in default namespace) + - User switches to accessible namespace (e.g., `kuadrant-test`) via NamespaceBar + - URL updates to `/kuadrant/ns/kuadrant-test/overview` + - Cards show namespace-scoped data + +3. **User switches namespace:** + - URL updates to `/kuadrant/ns/{new-namespace}/overview` + - All resource watches re-execute with new namespace + - Prometheus queries rebuild with new namespace filter + - Cards refresh with new data (gateways, policies, routes, and traffic metrics) + +4. **Direct URL access:** + - Works as shareable link + - No redirect on specific namespace URLs (e.g., `/kuadrant/ns/production/overview`) + - Base URL `/kuadrant/overview` performs RBAC check and redirects namespace-scoped users + +5. **Permission denied:** + - Individual cards show "Access Denied" (existing behavior) + - NamespaceBar automatically filters inaccessible namespaces + - Cluster-wide users cannot access `/kuadrant/overview` with insufficient permissions + +## Why This Approach + +**Consistency:** Matches existing Policies page pattern (uses NamespaceBar + URL routing) + +**Simplicity:** +- No custom namespace watching logic needed +- OpenShift SDK handles RBAC filtering automatically +- Minimal code changes +- Two routes instead of three (removed `/kuadrant/all-namespaces/overview`) +- No Project resource discovery required (reviewer feedback) + +**Performance:** +- No upfront namespace discovery required +- NamespaceBar handles lazy loading +- Resource watches only for selected namespace +- Prometheus queries filtered by namespace (reduces metric cardinality and query time) +- Metrics query utilities with PromQL injection protection + +**UX:** +- Standard OpenShift pattern (familiar to users) +- Shareable URLs +- Clear, predictable behavior +- Smart redirect based on RBAC +- Users can bookmark specific namespace views + +**Implementation Notes:** +- Originally designed with three routes (`/overview`, `/all-namespaces/overview`, `/ns/:ns/overview`) +- Simplified to two routes during implementation per reviewer feedback +- Cluster-wide users stay on `/kuadrant/overview` instead of redirecting to separate `/all-namespaces` route +- Namespace-scoped users redirect to `'default'` fallback, then switch to accessible namespace via NamespaceBar +- This reduces complexity and maintains consistent URL structure with the rest of the console + +## Alternatives Considered + +### Multi-Namespace Selection +**Rejected because:** +- Adds significant complexity (watching multiple namespaces) +- Performance concerns with many namespaces +- Not supported by NamespaceBar component +- Not needed for Policies page, not needed here + +### Custom Namespace Picker +**Rejected because:** +- Would require watching all Namespace resources +- Duplicate work (OpenShift SDK already provides this) +- Inconsistent with Policies page pattern + +### No Namespace Picker (Global Console Picker Only) +**Rejected because:** +- Would require URL structure without namespace parameter +- Less clear separation between cluster-wide and namespace-scoped views +- No shareable URLs + +## Success Criteria + +✅ Namespace-scoped users can view Overview page without errors +✅ Gateway health card shows accurate data for selected namespace +✅ All resource lists respect namespace selection +✅ Prometheus traffic metrics filter by namespace (verified by result count changes) +✅ RBAC checks work correctly for both cluster-wide and namespace-scoped +✅ Cluster-admins see identical experience by default +✅ Consistent with Policies page UX pattern +✅ Build passes without errors diff --git a/downstream.js b/downstream.js index f0b765bd..138f05fc 100755 --- a/downstream.js +++ b/downstream.js @@ -22,13 +22,13 @@ const replacements = { // Direct string replacements for links.ts // Order matters: specific URLs first to prevent partial matches 'https://docs.kuadrant.io/latest/kuadrant-operator/doc/user-guides/full-walkthrough/secure-protect-connect/': - `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html-single/configuring_and_deploying_gateway_policies_with_connectivity_link/index`, + `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html/configuring_and_deploying_gateway_policies/index`, 'https://docs.kuadrant.io/latest/kuadrant-operator/doc/observability/examples/': - `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html-single/connectivity_link_observability_guide/index`, + `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html/observability/index`, 'https://docs.kuadrant.io': `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/`, 'https://github.com/Kuadrant/kuadrant-operator/releases': - `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html-single/release_notes_for_connectivity_link/index`, + `https://docs.redhat.com/en/documentation/red_hat_connectivity_link/${version}/html/whats_new/rhcl-release-notes`, 'Kuadrant': 'Connectivity Link', }, }, diff --git a/e2e/tests/rbac.spec.ts b/e2e/tests/rbac.spec.ts index 5d9c6657..80178786 100644 --- a/e2e/tests/rbac.spec.ts +++ b/e2e/tests/rbac.spec.ts @@ -38,23 +38,43 @@ test.describe('RBAC - test-dev persona', () => { await expect(createButton).toBeHidden(); }); - // overview checks RBAC against 'default' namespace (resolves #ALL_NS# to default). - // namespace-scoped users only have permissions in kuadrant-test, so all overview - // cards show Access Denied. this is a known limitation - the overview has no - // namespace picker. - test('overview shows Access Denied for all cards (namespace-scoped user)', async ({ page }) => { + // namespace-scoped users get redirected from /kuadrant/overview to + // /kuadrant/ns/default/overview (fallback namespace when activeNamespace is #ALL_NS#). + test('overview redirects to namespace-scoped view (namespace-scoped user)', async ({ page }) => { await navigateToOverview(page); + await page.waitForLoadState('networkidle'); + + // verify we were redirected to namespace-scoped URL (uses 'default' as fallback) + await expect(page).toHaveURL(/\/kuadrant\/ns\/default\/overview/, { timeout: 15_000 }); + + // wait for any RBAC checks to complete await waitForPermissionsLoaded(page); - await expect( - page.locator('text=You do not have permission to view Policies'), - ).toBeVisible({ timeout: 15_000 }); + // user can then switch to kuadrant-test namespace using the console namespace dropdown + }); + + test('overview shows resources when accessed via kuadrant-test namespace', async ({ page }) => { + // navigate directly to their accessible namespace + await page.evaluate(() => { + window.history.pushState({}, '', '/kuadrant/ns/kuadrant-test/overview'); + window.dispatchEvent(new PopStateEvent('popstate')); + }); + await page.waitForLoadState('networkidle'); + + // should stay on this URL (no redirect since they have permissions) + await expect(page).toHaveURL('/kuadrant/ns/kuadrant-test/overview', { timeout: 15_000 }); + + await waitForPermissionsLoaded(page); + + // verify they can see gateway card (not access denied) + await expect(page.locator('text=Gateways - Traffic Analysis')).toBeVisible({ + timeout: 15_000, + }); + + // no "you do not have permission" messages await expect( page.locator('text=You do not have permission to view Gateways'), - ).toBeVisible(); - await expect( - page.locator('text=You do not have permission to view HTTPRoutes'), - ).toBeVisible(); + ).not.toBeVisible(); }); test('topology page shows no-permission view', async ({ page }) => { @@ -188,19 +208,41 @@ test.describe('RBAC - test-viewer persona', () => { await expect(createButton).toBeDisabled(); }); - test('overview shows Access Denied for all cards (namespace-scoped user)', async ({ page }) => { + test('overview redirects to namespace-scoped view (namespace-scoped user)', async ({ page }) => { await navigateToOverview(page); + await page.waitForLoadState('networkidle'); + + // verify we were redirected to namespace-scoped URL (uses 'default' as fallback) + await expect(page).toHaveURL(/\/kuadrant\/ns\/default\/overview/, { timeout: 15_000 }); + + // wait for any RBAC checks to complete await waitForPermissionsLoaded(page); - await expect( - page.locator('text=You do not have permission to view Policies'), - ).toBeVisible({ timeout: 15_000 }); - await expect( - page.locator('text=You do not have permission to view Gateways'), - ).toBeVisible(); - await expect( - page.locator('text=You do not have permission to view HTTPRoutes'), - ).toBeVisible(); + // user can then switch to kuadrant-test namespace using the console namespace dropdown + }); + + test('overview shows resources when accessed via kuadrant-test namespace', async ({ page }) => { + // navigate directly to their accessible namespace + await page.evaluate(() => { + window.history.pushState({}, '', '/kuadrant/ns/kuadrant-test/overview'); + window.dispatchEvent(new PopStateEvent('popstate')); + }); + await page.waitForLoadState('networkidle'); + + // should stay on this URL (no redirect since they have permissions) + await expect(page).toHaveURL('/kuadrant/ns/kuadrant-test/overview', { timeout: 15_000 }); + + await waitForPermissionsLoaded(page); + + // verify they can see gateway card (not access denied) + await expect(page.locator('text=Gateways - Traffic Analysis')).toBeVisible({ + timeout: 15_000, + }); + + // create buttons should be disabled (read-only user) + const createGateway = page.locator('button:has-text("Create Gateway")'); + await expect(createGateway).toBeVisible(); + await expect(createGateway).toBeDisabled(); }); }); @@ -261,15 +303,41 @@ test.describe('RBAC - test-devops persona', () => { } }); - test('overview shows Access Denied for all cards (namespace-scoped user)', async ({ page }) => { + test('overview redirects to namespace-scoped view (namespace-scoped user)', async ({ page }) => { await navigateToOverview(page); + await page.waitForLoadState('networkidle'); + + // verify we were redirected to namespace-scoped URL (uses 'default' as fallback) + await expect(page).toHaveURL(/\/kuadrant\/ns\/default\/overview/, { timeout: 15_000 }); + + // wait for any RBAC checks to complete await waitForPermissionsLoaded(page); - // even though test-devops has policy list in kuadrant-test, the overview - // checks against 'default' namespace - await expect( - page.locator('text=You do not have permission to view Gateways'), - ).toBeVisible({ timeout: 15_000 }); + // user can then switch to kuadrant-test namespace using the console namespace dropdown + }); + + test('overview shows resources when accessed via kuadrant-test namespace', async ({ page }) => { + // navigate directly to their accessible namespace + await page.evaluate(() => { + window.history.pushState({}, '', '/kuadrant/ns/kuadrant-test/overview'); + window.dispatchEvent(new PopStateEvent('popstate')); + }); + await page.waitForLoadState('networkidle'); + + // should stay on this URL (no redirect since they have permissions) + await expect(page).toHaveURL('/kuadrant/ns/kuadrant-test/overview', { timeout: 15_000 }); + + await waitForPermissionsLoaded(page); + + // verify they can see gateway card (not access denied) + await expect(page.locator('text=Gateways - Traffic Analysis')).toBeVisible({ + timeout: 15_000, + }); + + // verify create policy dropdown is enabled (has CRUD on policies) + const createButton = page.locator('button:has-text("Create Policy")'); + await expect(createButton).toBeVisible(); + await expect(createButton).toBeEnabled(); }); }); @@ -313,6 +381,22 @@ test.describe('RBAC - test-admin persona', () => { } }); + test('overview stays on cluster-wide view (no redirect for cluster-admin)', async ({ page }) => { + await navigateToOverview(page); + await page.waitForLoadState('networkidle'); + + // verify admin users stay on /kuadrant/overview (cluster-wide view, no redirect) + await expect(page).toHaveURL('/kuadrant/overview', { timeout: 15_000 }); + + // wait for any RBAC checks to complete + await waitForPermissionsLoaded(page); + + // verify they can see resources (have cluster-wide permissions) + await expect( + page.locator('text=You do not have permission to view Gateways'), + ).not.toBeVisible(); + }); + test('overview shows all cards with create buttons enabled', async ({ page }) => { await navigateToOverview(page); await waitForPermissionsLoaded(page); @@ -356,4 +440,67 @@ test.describe('RBAC - test-admin persona', () => { ).not.toBeVisible({ timeout: 15_000 }); await expect(page.locator('text=Topology View')).toBeVisible({ timeout: 15_000 }); }); + + test('namespace picker changes to namespace-scoped view', async ({ page }) => { + await navigateToOverview(page); + await waitForPermissionsLoaded(page); + + // verify we're on cluster-wide view + await expect(page).toHaveURL('/kuadrant/overview', { timeout: 15_000 }); + + // find the namespace dropdown button using various selectors + const namespaceButtonSelectors = [ + 'button[data-test="namespace-dropdown-toggle"]', + 'button[data-test="namespace-bar-dropdown"]', + '[data-test-id="namespace-bar-dropdown"] button', + '.co-namespace-selector button', + '.co-namespace-bar button', + ]; + + const namespaceButton = await (async () => { + for (const selector of namespaceButtonSelectors) { + const element = page.locator(selector).first(); + if ((await element.count()) > 0 && (await element.isVisible())) { + return element; + } + } + throw new Error('Could not locate namespace picker button'); + })(); + + // click to open dropdown + await namespaceButton.click(); + await page.waitForTimeout(1000); + + // select kuadrant-test namespace + const namespaceOptionSelectors = [ + 'li:has-text("kuadrant-test")', + 'button:has-text("kuadrant-test")', + 'a:has-text("kuadrant-test")', + '[role="option"]:has-text("kuadrant-test")', + ]; + + const namespaceOption = await (async () => { + for (const selector of namespaceOptionSelectors) { + const element = page.locator(selector).first(); + if ((await element.count()) > 0 && (await element.isVisible())) { + return element; + } + } + throw new Error('Could not locate kuadrant-test namespace option'); + })(); + + await namespaceOption.click(); + await page.waitForTimeout(2000); + + // verify URL changed to namespace-scoped view + await expect(page).toHaveURL('/kuadrant/ns/kuadrant-test/overview', { + timeout: 15_000, + }); + + // verify URL doesn't revert back to cluster-wide view + await page.waitForTimeout(2000); + await expect(page).toHaveURL('/kuadrant/ns/kuadrant-test/overview', { + timeout: 5_000, + }); + }); }); diff --git a/entrypoint.sh b/entrypoint.sh index 0a4ba335..d1ec4727 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,10 +1,11 @@ #!/bin/bash -# Inject topology ConfigMap location +# Inject topology ConfigMap location and metrics configuration cat < /tmp/config.js window.kuadrant_config = { - TOPOLOGY_CONFIGMAP_NAME: '${TOPOLOGY_CONFIGMAP_NAME}', - TOPOLOGY_CONFIGMAP_NAMESPACE: '${TOPOLOGY_CONFIGMAP_NAMESPACE}' + TOPOLOGY_CONFIGMAP_NAME: '${TOPOLOGY_CONFIGMAP_NAME:-topology}', + TOPOLOGY_CONFIGMAP_NAMESPACE: '${TOPOLOGY_CONFIGMAP_NAMESPACE:-kuadrant-system}', + METRICS_WORKLOAD_SUFFIX: '${METRICS_WORKLOAD_SUFFIX:-openshift-default}' }; EOF diff --git a/install.yaml b/install.yaml index 30b1c4eb..76c0998a 100644 --- a/install.yaml +++ b/install.yaml @@ -36,6 +36,8 @@ spec: value: topology - name: TOPOLOGY_CONFIGMAP_NAMESPACE value: kuadrant-system + - name: METRICS_WORKLOAD_SUFFIX + value: -openshift-default volumeMounts: - name: plugin-serving-cert readOnly: true diff --git a/kuadrant-dev-setup/cluster-monitoring.yaml b/kuadrant-dev-setup/cluster-monitoring.yaml new file mode 100644 index 00000000..0d768886 --- /dev/null +++ b/kuadrant-dev-setup/cluster-monitoring.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: cluster-monitoring-config + namespace: openshift-monitoring +data: + config.yaml: | + enableUserWorkload: true diff --git a/locales/en/plugin__kuadrant-console-plugin.json b/locales/en/plugin__kuadrant-console-plugin.json index a5d08fd0..6a8c2e24 100644 --- a/locales/en/plugin__kuadrant-console-plugin.json +++ b/locales/en/plugin__kuadrant-console-plugin.json @@ -23,6 +23,7 @@ "Configuring and deploying Gateway policies with Kuadrant": "Configuring and deploying Gateway policies with Kuadrant", "Conflicted": "Conflicted", "Create": "Create", + "Create {{policyType}}": "Create {{policyType}}", "Create AuthPolicy": "Create AuthPolicy", "Create DNS Policy": "Create DNS Policy", "Create Gateway": "Create Gateway", @@ -48,6 +49,7 @@ "Endpoint": "Endpoint", "Enforced": "Enforced", "Error Codes": "Error Codes", + "Error creating {{policyType}}": "Error creating {{policyType}}", "Error creating AuthPolicy": "Error creating AuthPolicy", "Error creating OIDC Policy": "Error creating OIDC Policy", "Error creating Plan Policy": "Error creating Plan Policy", @@ -57,6 +59,7 @@ "Error parsing topology:": "Error parsing topology:", "Error parsing YAML:": "Error parsing YAML:", "Error Rate": "Error Rate", + "Error updating {{policyType}}": "Error updating {{policyType}}", "Expand Getting Started": "Expand Getting Started", "Failure Threshold": "Failure Threshold", "Feature Highlights": "Feature Highlights", @@ -178,6 +181,7 @@ "Weight value to apply to weighted endpoints default: 120": "Weight value to apply to weighted endpoints default: 120", "YAML View": "YAML View", "You can view and create HTTPRoutes": "You can view and create HTTPRoutes", + "You do not have permission to create a {{policyType}}": "You do not have permission to create a {{policyType}}", "You do not have permission to view Gateways": "You do not have permission to view Gateways", "You do not have permission to view HTTPRoutes": "You do not have permission to view HTTPRoutes", "You do not have permission to view Policies": "You do not have permission to view Policies", diff --git a/package.json b/package.json index 464a2383..54790798 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kuadrant-console-plugin", - "version": "0.3.6", + "version": "0.3.7", "description": "Kuadrant OpenShift Console plugin", "private": true, "license": "Apache-2.0", @@ -27,7 +27,8 @@ }, "resolutions": { "http-proxy-middleware": "2.0.7", - "lodash": "4.17.23", + "immutable": "3.8.3", + "lodash": "4.18.0", "node-forge": "^1.3.2", "qs": "6.14.1" }, @@ -73,7 +74,7 @@ }, "consolePlugin": { "name": "kuadrant-console-plugin", - "version": "0.3.6", + "version": "0.3.7", "displayName": "Kuadrant OpenShift Console Plugin", "description": "Kuadrant OpenShift Console Plugin", "latestSupportedOpenshiftVersion": "4.19", diff --git a/src/components/DropdownWithKebab.tsx b/src/components/DropdownWithKebab.tsx index 78b72b9e..52c14ab4 100644 --- a/src/components/DropdownWithKebab.tsx +++ b/src/components/DropdownWithKebab.tsx @@ -16,7 +16,7 @@ import { } from '@patternfly/react-core'; import { k8sDelete, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { RESOURCES, ResourceKind } from '../utils/resources'; import useAccessReviews from '../utils/resourceRBAC'; import { getModelFromResource, getResourceNameFromKind } from '../utils/getModelFromResource'; @@ -27,7 +27,7 @@ type DropdownWithKebabProps = { const DropdownWithKebab: React.FC = ({ obj }) => { const [isOpen, setIsOpen] = React.useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); - const history = useHistory(); + const navigate = useNavigate(); const model = getModelFromResource(obj); const onToggleClick = () => { @@ -75,13 +75,13 @@ const DropdownWithKebab: React.FC = ({ obj }) => { obj.kind === 'Gateway' || obj.kind === 'HTTPRoute' ) { - history.push({ + navigate({ pathname: `/k8s/ns/${obj.metadata.namespace}/${obj.apiVersion.replace('/', '~')}~${ obj.kind }/${obj.metadata.name}/yaml`, }); } else { - history.push({ + navigate({ pathname: `/k8s/ns/${obj.metadata.namespace}/${policyType}/name/${obj.metadata.name}/edit`, }); } diff --git a/src/components/GatewayPoliciesPage.tsx b/src/components/GatewayPoliciesPage.tsx index d5a8330b..ec67712d 100644 --- a/src/components/GatewayPoliciesPage.tsx +++ b/src/components/GatewayPoliciesPage.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { PageSection, Title } from '@patternfly/react-core'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import './kuadrant.css'; import AssociatedResourceList from './AssociatedResourceList'; import { diff --git a/src/components/HTTPRoutePoliciesPage.tsx b/src/components/HTTPRoutePoliciesPage.tsx index 378b34b3..455db6bc 100644 --- a/src/components/HTTPRoutePoliciesPage.tsx +++ b/src/components/HTTPRoutePoliciesPage.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { PageSection, Title } from '@patternfly/react-core'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import './kuadrant.css'; import AssociatedResourceList from './AssociatedResourceList'; import { diff --git a/src/components/KuadrantCreateUpdate.tsx b/src/components/KuadrantCreateUpdate.tsx index 8066636f..8a730db1 100644 --- a/src/components/KuadrantCreateUpdate.tsx +++ b/src/components/KuadrantCreateUpdate.tsx @@ -8,13 +8,13 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import { useTranslation } from 'react-i18next'; import { Button, AlertVariant, Alert, AlertGroup } from '@patternfly/react-core'; -import { History } from 'history'; +import { NavigateFunction } from 'react-router-dom-v5-compat'; interface GenericPolicyForm { model: K8sModel; resource: K8sResourceCommon; policyType: string; - history: History; + navigate: NavigateFunction; validation: boolean; } @@ -22,7 +22,7 @@ const KuadrantCreateUpdate: React.FC = ({ model, resource, policyType, - history, + navigate, validation, }) => { const { t } = useTranslation('plugin__kuadrant-console-plugin'); @@ -34,37 +34,45 @@ const KuadrantCreateUpdate: React.FC = ({ setErrorAlertMsg(''); try { - if (update == true) { + if (update) { const response = await k8sUpdate({ model: model, data: resource, }); console.log(`${policyType} updated successfully:`, response); - history.push(`/kuadrant/all-namespaces/policies/${policyType}`); + navigate(`/kuadrant/all-namespaces/policies/${policyType}`); } else { const response = await k8sCreate({ model: model, data: resource, }); console.log(`${policyType} created successfully:`, response); - history.push(`/kuadrant/all-namespaces/policies/${policyType}`); + navigate(`/kuadrant/all-namespaces/policies/${policyType}`); } - } catch (error) { - if (update == true) { - console.error(t(`Cannot update ${policyType}`, error)); - setErrorAlertMsg(error.message); - } - { - console.error(t(`Cannot create ${policyType}`, error)); - setErrorAlertMsg(error.message); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'An error occurred'; + + if (update) { + console.error(`Cannot update ${policyType}:`, error); + } else { + console.error(`Cannot create ${policyType}:`, error); } + setErrorAlertMsg(message); } }; return ( <> {errorAlertMsg != '' && ( - + {errorAlertMsg} diff --git a/src/components/KuadrantDNSPolicyCreatePage.tsx b/src/components/KuadrantDNSPolicyCreatePage.tsx index ff2e27f5..758483e6 100644 --- a/src/components/KuadrantDNSPolicyCreatePage.tsx +++ b/src/components/KuadrantDNSPolicyCreatePage.tsx @@ -24,7 +24,7 @@ import { K8sResourceCommon, useActiveNamespace, } from '@openshift-console/dynamic-plugin-sdk'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom-v5-compat'; import { LoadBalancing, HealthCheck } from './dnspolicy/types'; import LoadBalancingField from './dnspolicy/LoadBalancingField'; import HealthCheckField from './dnspolicy/HealthCheckField'; @@ -135,7 +135,7 @@ const KuadrantDNSPolicyCreatePage: React.FC = () => { kind: dnsPolicyGVK.kind, }); - const history = useHistory(); + const navigate = useNavigate(); interface dnsPolicyEdit extends K8sResourceCommon { spec?: { @@ -266,7 +266,7 @@ const KuadrantDNSPolicyCreatePage: React.FC = () => { }; const handleCancelResource = () => { - handleCancel(selectedNamespace, dnsPolicy, history); + handleCancel(selectedNamespace, dnsPolicy, navigate); }; const formValidation = () => { if ( @@ -391,7 +391,7 @@ const KuadrantDNSPolicyCreatePage: React.FC = () => { model={dnsPolicyModel} resource={dnsPolicy} policyType="dns" - history={history} + navigate={navigate} validation={formValidation()} /> + + ) : ( - ) : ( - - - )} @@ -889,8 +990,8 @@ const KuadrantOverviewPage: React.FC = () => { resources={[resourceGVKMapping['Gateway']]} columns={gatewayTrafficColumns} renderers={gatewayTrafficRenders} - namespace="#ALL_NS#" - emtpyResourceName="Gateways" + namespace={watchNamespace} + emptyResourceName="Gateways" /> @@ -980,7 +1081,9 @@ const KuadrantOverviewPage: React.FC = () => { ) : ( {t(policy)} @@ -1003,7 +1106,7 @@ const KuadrantOverviewPage: React.FC = () => { resourceGVKMapping['TLSPolicy'], ]} columns={columns} - namespace="#ALL_NS#" + namespace={watchNamespace} paginationLimit={5} /> @@ -1016,27 +1119,27 @@ const KuadrantOverviewPage: React.FC = () => { {t('HTTPRoutes')} - {resourceRBAC['HTTPRoute']?.create ? ( + {!resourceRBAC['HTTPRoute']?.create ? ( + + + + ) : ( - ) : ( - - - )} diff --git a/src/components/KuadrantPoliciesPage.tsx b/src/components/KuadrantPoliciesPage.tsx index 2cc936fa..9226b46d 100644 --- a/src/components/KuadrantPoliciesPage.tsx +++ b/src/components/KuadrantPoliciesPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { useTranslation } from 'react-i18next'; import { sortable } from '@patternfly/react-table'; import { @@ -107,7 +107,7 @@ export const AllPoliciesListPage: React.FC<{ } const [isOpen, setIsOpen] = useState(false); - const history = useHistory(); + const navigate = useNavigate(); const onToggleClick = () => setIsOpen(!isOpen); @@ -115,7 +115,7 @@ export const AllPoliciesListPage: React.FC<{ const resource = resourceGVKMapping[policyType]; const resolvedNamespace = activeNamespace === '#ALL_NS#' ? 'default' : activeNamespace; const targetUrl = `/k8s/ns/${resolvedNamespace}/${resource.group}~${resource.version}~${resource.kind}/~new`; - history.push(targetUrl); + navigate(targetUrl); setIsOpen(false); // Close the dropdown after selecting an option }; @@ -135,7 +135,10 @@ export const AllPoliciesListPage: React.FC<{ {t(policy)} ) : ( - + {t(policy)} @@ -197,6 +200,8 @@ const PoliciesListPage: React.FC<{ ); } + const createButtonText = t('Create {{policyType}}', { policyType: resource.gvk.kind }); + return ( <> @@ -207,15 +212,19 @@ const PoliciesListPage: React.FC<{ - {t(`plugin__kuadrant-console-plugin~Create ${resource.gvk.kind}`)} + {createButtonText} ) : ( - + - {t(`Create ${resource.gvk.kind}`)} + {createButtonText} )} @@ -226,12 +235,116 @@ const PoliciesListPage: React.FC<{ ); }; +// Context to pass data to tab components without prop drilling +const PolicyPageContext = React.createContext<{ + activeNamespace: string; + defaultColumns: TableColumn[]; + resourceRBAC: RBACMap; +} | null>(null); + +const usePolicyPageContext = () => { + const context = React.useContext(PolicyPageContext); + if (!context) { + throw new Error('usePolicyPageContext must be used within PolicyPageContext.Provider'); + } + return context; +}; + +// Tab components that read from context - stable references +const AllPoliciesTab: React.FC = () => { + const { activeNamespace, defaultColumns, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const AuthPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const RateLimitPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const TokenRateLimitPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const OIDCPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const PlanPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const DNSPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + +const TLSPolicyTab: React.FC = () => { + const { activeNamespace, resourceRBAC } = usePolicyPageContext(); + return ( + + ); +}; + const KuadrantPoliciesPage: React.FC = () => { const { t } = useTranslation('plugin__kuadrant-console-plugin'); const { ns } = useParams<{ ns: string }>(); const [activeNamespace, setActiveNamespace] = useActiveNamespace(); const [activePerspective] = useActivePerspective(); - const history = useHistory(); + const navigate = useNavigate(); React.useEffect(() => { if (ns && ns !== activeNamespace) { @@ -295,85 +408,27 @@ const KuadrantPoliciesPage: React.FC = () => { const policyRBACNil = policyKinds.every((policy) => !resourceRBAC[policy]?.list); - const All: React.FC = () => ( - - ); - - const Auth: React.FC = () => ( - - ); - - const RateLimit: React.FC = () => ( - - ); - const TokenRateLimit: React.FC = () => ( - - ); - const OIDC: React.FC = () => ( - - ); - const Plan: React.FC = () => ( - - ); + // Use stable component references - these don't change on re-render let pages = [ { href: '', name: t('All Policies'), - component: All, + component: AllPoliciesTab, }, ]; if (activePerspective === 'admin') { - const DNS: React.FC = () => ( - - ); - const TLS: React.FC = () => ( - - ); - pages = [ ...pages, { href: 'dns', name: t('DNS'), - component: DNS, + component: DNSPolicyTab, }, { href: 'tls', name: t('TLS'), - component: TLS, + component: TLSPolicyTab, }, ]; } @@ -383,27 +438,27 @@ const KuadrantPoliciesPage: React.FC = () => { { href: 'auth', name: t('Auth'), - component: Auth, + component: AuthPolicyTab, }, { href: 'ratelimit', name: t('RateLimit'), - component: RateLimit, + component: RateLimitPolicyTab, }, { href: 'tokenratelimit', name: t('TokenRateLimit'), - component: TokenRateLimit, + component: TokenRateLimitPolicyTab, }, { href: 'oidc', name: t('OIDC'), - component: OIDC, + component: OIDCPolicyTab, }, { href: 'plan', name: t('Plan'), - component: Plan, + component: PlanPolicyTab, }, ]; @@ -420,7 +475,13 @@ const KuadrantPoliciesPage: React.FC = () => { currentTab = `/kuadrant/all-namespaces/policies/${activeTab}`; } - history.replace(currentTab); + navigate(currentTab, { replace: true }); + }; + + const contextValue = { + activeNamespace, + defaultColumns, + resourceRBAC, }; return ( @@ -432,7 +493,9 @@ const KuadrantPoliciesPage: React.FC = () => { {policyRBACNil ? ( ) : ( - + + + )} ); diff --git a/src/components/KuadrantStatusAlert.tsx b/src/components/KuadrantStatusAlert.tsx index 12ec1cc9..03dd0f73 100644 --- a/src/components/KuadrantStatusAlert.tsx +++ b/src/components/KuadrantStatusAlert.tsx @@ -54,7 +54,7 @@ export const KuadrantStatusAlert: React.FC = React.memo(() => { if (err) { return (
- +
@@ -92,7 +92,7 @@ export const KuadrantStatusAlert: React.FC = React.memo(() => { position="top" content={
-
{t(missingDependency[0].message)}
+
{missingDependency[0].message}
{t('Reason: ')} {missingDependency[0].reason}
@@ -123,7 +123,7 @@ export const KuadrantStatusAlert: React.FC = React.memo(() => { position="top" content={
-
{t(success[0].message)}
+
{success[0].message}
{t('Reason: ')} {success[0].reason}
diff --git a/src/components/KuadrantTLSCreatePage.tsx b/src/components/KuadrantTLSCreatePage.tsx index 1c6c204b..3e211762 100644 --- a/src/components/KuadrantTLSCreatePage.tsx +++ b/src/components/KuadrantTLSCreatePage.tsx @@ -22,7 +22,7 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import './kuadrant.css'; import { handleCancel } from '../utils/cancel'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom-v5-compat'; import yaml from 'js-yaml'; import { useTranslation } from 'react-i18next'; import ClusterIssuerSelect from './issuer/clusterIssuerSelect'; @@ -36,7 +36,7 @@ import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; import { resourceGVKMapping } from '../utils/resources'; const KuadrantTLSCreatePage: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const [policyName, setPolicyName] = React.useState(''); const [selectedNamespace] = useActiveNamespace(); const [selectedGateway, setSelectedGateway] = React.useState({ @@ -210,7 +210,7 @@ const KuadrantTLSCreatePage: React.FC = () => { //Cancel const handleCancelResource = () => { - handleCancel(selectedNamespace, tlsPolicy, history); + handleCancel(selectedNamespace, tlsPolicy, navigate); }; if ( @@ -323,7 +323,7 @@ const KuadrantTLSCreatePage: React.FC = () => { model={tlsPolicyModel} resource={tlsPolicy} policyType="tls" - history={history} + navigate={navigate} validation={isFormValid} />