diff --git a/base/202-extension-crd.yaml b/base/202-extension-crd.yaml index 5f599c3f2..0dba28a06 100644 --- a/base/202-extension-crd.yaml +++ b/base/202-extension-crd.yaml @@ -47,7 +47,7 @@ spec: jsonPath: .spec.name - name: Display name type: string - jsonPath: .spec.displayname + jsonPath: .spec.displayName - name: Age type: date jsonPath: .metadata.creationTimestamp diff --git a/docs/extensions.md b/docs/extensions.md index 916869652..923247d1b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -41,12 +41,13 @@ See the [Example: Register a CronJob extension](#example-register-a-cronjob-exte #### ExtensionSpec -| Variable Name | Type | Required | Default | Description | -|---------------|-------------------|----------|---------|------------------------------------------------------------------| -| apiVersion | string | Yes | - | Extension resource group | -| name | string | Yes | - | Extension resource name | -| displayname | string | Yes | - | Display name in the Dashboard UI | -| namespaced | boolean | No | true | Specifies whether the Extension represents a namespaced resource | +| Variable Name | Type | Required | Default | Description | +|-----------------------------|-------------------|----------|---------|------------------------------------------------------------------| +| apiVersion | string | Yes | - | Extension resource group | +| name | string | Yes | - | Extension resource name | +| disableResourceDetailsLinks | boolean | No | false | Disable display of links to resource details pages | +| displayName | string | Yes | - | Display name in the Dashboard UI | +| namespaced | boolean | No | true | Specifies whether the Extension represents a namespaced resource | ### Example: Register a CronJob extension @@ -61,7 +62,7 @@ metadata: spec: apiVersion: batch/v1 name: cronjobs - displayname: k8s cronjobs + displayName: k8s cronjobs EOF ``` diff --git a/packages/e2e/cypress/e2e/common/extensions.cy.js b/packages/e2e/cypress/e2e/common/extensions.cy.js index 1058ab115..5bf0d22e3 100644 --- a/packages/e2e/cypress/e2e/common/extensions.cy.js +++ b/packages/e2e/cypress/e2e/common/extensions.cy.js @@ -37,7 +37,7 @@ metadata: spec: apiVersion: core/v1 name: namespaces - displayname: Namespaces + displayName: Namespaces `); cy.contains(`.${carbonPrefix}--side-nav a`, 'Namespaces').click(); diff --git a/src/api/extensions.js b/src/api/extensions.js index 6004553f5..44b7ef00d 100644 --- a/src/api/extensions.js +++ b/src/api/extensions.js @@ -25,12 +25,19 @@ export function useExtensions(params, queryConfig) { return { ...query, data: (data || []).map(({ spec }) => { - const { displayname: displayName, name, namespaced } = spec; + const { + disableResourceDetailsLinks, + displayname, // keep for backwards compatibility for a few releases + displayName, + name, + namespaced + } = spec; const [apiGroup, apiVersion] = spec.apiVersion.split('/'); return { apiGroup, apiVersion, - displayName, + disableResourceDetailsLinks, + displayName: displayName || displayname, name, namespaced }; diff --git a/src/api/extensions.test.js b/src/api/extensions.test.js index 0e5a859a6..99f5f2960 100644 --- a/src/api/extensions.test.js +++ b/src/api/extensions.test.js @@ -15,6 +15,40 @@ import * as API from './extensions'; import * as utils from './utils'; it('useExtensions', () => { + const name = 'fake_name'; + const group = 'fake_group'; + const version = 'fake_version'; + const apiVersion = `${group}/${version}`; + const displayName = 'fake_displayName'; + const namespaced = true; + const query = { + data: [{ spec: { apiVersion, displayName, name, namespaced } }] + }; + const params = { fake: 'params' }; + vi.spyOn(utils, 'useCollection').mockImplementation(() => query); + const extensions = API.useExtensions(params); + expect(utils.useCollection).toHaveBeenCalledWith( + expect.objectContaining({ + group: utils.dashboardAPIGroup, + kind: 'extensions', + params, + version: 'v1alpha1' + }) + ); + expect(extensions).toEqual({ + data: [ + { + apiGroup: group, + apiVersion: version, + displayName, + name, + namespaced + } + ] + }); +}); + +it('useExtensions displayname backwards compatibility', () => { const name = 'fake_name'; const group = 'fake_group'; const version = 'fake_version'; diff --git a/src/containers/ResourceList/ResourceList.jsx b/src/containers/ResourceList/ResourceList.jsx index 9e7c5551f..4b4a09226 100644 --- a/src/containers/ResourceList/ResourceList.jsx +++ b/src/containers/ResourceList/ResourceList.jsx @@ -25,7 +25,9 @@ import ListPageLayout from '../ListPageLayout'; import { useAPIResource, useCustomResources, - useSelectedNamespace + useExtensions, + useSelectedNamespace, + useTenantNamespaces } from '../../api'; export function ResourceListContainer() { @@ -35,18 +37,29 @@ export function ResourceListContainer() { const matches = useMatches(); const params = useParams(); + const tenantNamespaces = useTenantNamespaces(); + const { data: extensions = [] } = useExtensions( + { + namespace: tenantNamespaces[0] || ALL_NAMESPACES + }, + { disableWebSocket: true, retryOnMount: false } + ); + const { namespace: namespaceParam } = params; const match = matches.at(-1); const handle = match.handle || {}; let { group, kind, version } = handle; const { resourceURL, title } = handle; - let isExtension = false; + let extension; if (!(group && kind && version)) { // we're on a kubernetes resource extension page // grab values directly from the URL ({ group, kind, version } = params); - isExtension = true; + extension = extensions?.find( + ({ apiGroup, apiVersion, name }) => + apiGroup === group && apiVersion === version && kind === name + ); } const { selectedNamespace } = useSelectedNamespace(); @@ -59,12 +72,12 @@ export function ResourceListContainer() { data: apiResource, error: apiResourceError, isLoading: isLoadingAPIResource - } = useAPIResource({ group, kind, version }, { enabled: isExtension }); - const isNamespaced = isExtension + } = useAPIResource({ group, kind, version }, { enabled: !!extension }); + const isNamespaced = extension ? !isLoadingAPIResource && apiResource?.namespaced : handle?.isNamespaced; - if (isExtension && typeof apiResource?.namespaced !== 'undefined') { + if (extension && typeof apiResource?.namespaced !== 'undefined') { // dynamically toggle the namespace dropdown behaviour depending on // whether the kind is namespaced or cluster-scoped match.handle.isNamespaced = isNamespaced; @@ -83,7 +96,7 @@ export function ResourceListContainer() { version }, { - enabled: !isExtension || (!isLoadingAPIResource && !apiResourceError) + enabled: !extension || (!isLoadingAPIResource && !apiResourceError) } ); @@ -176,7 +189,7 @@ export function ResourceListContainer() { }) } ].filter(Boolean)} - loading={(isExtension && isLoadingAPIResource) || isLoadingResources} + loading={(extension && isLoadingAPIResource) || isLoadingResources} rows={paginatedResources.map(resource => { const { creationTimestamp, @@ -185,15 +198,19 @@ export function ResourceListContainer() { uid } = resource.metadata; + let resourceLink; + if (!extension?.disableResourceDetailsLinks) { + resourceLink = getResourceURL({ name, resourceNamespace }); + } + return { id: uid, - name: ( - + name: resourceLink ? ( + {name} + ) : ( + name ), namespace: resourceNamespace, createdTime: