From 30a3afc74b3f48383a18676a232ae5e7c188e77d Mon Sep 17 00:00:00 2001 From: Christian Karidas <105549337+chriskari@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:32:42 +0100 Subject: [PATCH] feat: custom busola extensions (#3523) * feat: add proxyhandler to backend * feat: adjust busola code to handle custom extensions * feat: added example custom extension * feat: rename folder * feat: add docs * feat: update docs * feat: update docs * feat: add more specific types * feat: adjust to review comments * adjust to comments --- backend/common.js | 10 +- backend/index.js | 7 +- backend/proxy.js | 45 ++++ docs/extensibility/README.md | 2 + docs/extensibility/custom-extensions.md | 24 ++ docs/features.md | 9 + examples/custom-extension/README.md | 44 ++++ .../deploy-custom-extension.sh | 4 + examples/custom-extension/general.yaml | 10 + examples/custom-extension/kustomization.yaml | 11 + examples/custom-extension/script.js | 220 ++++++++++++++++++ examples/custom-extension/ui.html | 6 + kyma/environments/dev/config.yaml | 2 + kyma/environments/prod/config.yaml | 2 + kyma/environments/stage/config.yaml | 2 + public/defaultConfig.yaml | 2 + .../Extensibility/ExtensibilityList.js | 42 +++- src/state/navigation/extensionsAtom.ts | 47 ++++ src/state/types.ts | 3 + 19 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 backend/proxy.js create mode 100644 docs/extensibility/custom-extensions.md create mode 100644 examples/custom-extension/README.md create mode 100755 examples/custom-extension/deploy-custom-extension.sh create mode 100644 examples/custom-extension/general.yaml create mode 100644 examples/custom-extension/kustomization.yaml create mode 100644 examples/custom-extension/script.js create mode 100644 examples/custom-extension/ui.html diff --git a/backend/common.js b/backend/common.js index 4a562926e1..0129f0d01b 100644 --- a/backend/common.js +++ b/backend/common.js @@ -142,10 +142,12 @@ function extractHeadersData(req) { const clientCAHeader = 'x-client-certificate-data'; const clientKeyDataHeader = 'x-client-key-data'; const authorizationHeader = 'x-k8s-authorization'; - - const targetApiServer = handleDockerDesktopSubsitution( - new URL(req.headers[urlHeader]), - ); + let targetApiServer; + if (req.headers[urlHeader]) { + targetApiServer = handleDockerDesktopSubsitution( + new URL(req.headers[urlHeader]), + ); + } const ca = decodeHeaderToBuffer(req.headers[caHeader]) || certs; const cert = decodeHeaderToBuffer(req.headers[clientCAHeader]); const key = decodeHeaderToBuffer(req.headers[clientKeyDataHeader]); diff --git a/backend/index.js b/backend/index.js index d98c47589a..82934c016d 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,4 +1,5 @@ import { makeHandleRequest, serveStaticApp, serveMonaco } from './common'; +import { proxyHandler } from './proxy.js'; import { handleTracking } from './tracking.js'; import jsyaml from 'js-yaml'; //import { requestLogger } from './utils/other'; //uncomment this to log the outgoing traffic @@ -53,6 +54,8 @@ if (process.env.NODE_ENV === 'development') { app.use(cors({ origin: '*' })); } +app.get('/proxy', proxyHandler); + let server = null; if ( @@ -81,13 +84,15 @@ const isDocker = process.env.IS_DOCKER === 'true'; const handleRequest = makeHandleRequest(); if (isDocker) { + // Running in dev mode // yup, order matters here serveMonaco(app); app.use('/backend', handleRequest); serveStaticApp(app, '/', '/core-ui'); } else { + // Running in prod mode handleTracking(app); - app.use(handleRequest); + app.use('/backend', handleRequest); } process.on('SIGINT', function() { diff --git a/backend/proxy.js b/backend/proxy.js new file mode 100644 index 0000000000..ad4a35c94b --- /dev/null +++ b/backend/proxy.js @@ -0,0 +1,45 @@ +import { request as httpsRequest } from 'https'; +import { request as httpRequest } from 'http'; +import { URL } from 'url'; + +async function proxyHandler(req, res) { + const targetUrl = req.query.url; + if (!targetUrl) { + return res.status(400).send('Target URL is required as a query parameter'); + } + + try { + const parsedUrl = new URL(targetUrl); + const isHttps = parsedUrl.protocol === 'https:'; + const libRequest = isHttps ? httpsRequest : httpRequest; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: req.method, + headers: { ...req.headers, host: parsedUrl.host }, + }; + + const proxyReq = libRequest(options, proxyRes => { + // Forward status and headers from the target response + res.writeHead(proxyRes.statusCode, proxyRes.headers); + // Pipe the response data from the target back to the client + proxyRes.pipe(res); + }); + + proxyReq.on('error', () => { + res.status(500).send('An error occurred while making the proxy request.'); + }); + + if (Buffer.isBuffer(req.body)) { + proxyReq.end(req.body); // If the body is already buffered, use it directly. + } else { + req.pipe(proxyReq); // Otherwise, pipe the request for streamed or chunked data. + } + } catch (error) { + res.status(500).send('An error occurred while processing the request.'); + } +} + +export { proxyHandler }; diff --git a/docs/extensibility/README.md b/docs/extensibility/README.md index c3dcc60050..011cea37a2 100644 --- a/docs/extensibility/README.md +++ b/docs/extensibility/README.md @@ -4,6 +4,8 @@ With Busola's extensibility feature, you can create a dedicated user interface (UI) page for your CustomResourceDefinition (CRD). It enables you to add navigation nodes, on cluster or namespace level, and to configure your [UI display](./30-details-summary.md), for example, a resource list page, and details pages. You can also [create and edit forms](./40-form-fields.md). To create a UI component, you need a ConfigMap. +You can also leverage Busola's [custom extension feature](./custom-extensions.md) to design entirely custom user interfaces tailored to your specific needs. + ## Create a ConfigMap for Your UI To create a ConfigMap with your CRD's UI configuration, you can either use the Extensions feature or do it manually. diff --git a/docs/extensibility/custom-extensions.md b/docs/extensibility/custom-extensions.md new file mode 100644 index 0000000000..ef645b8a57 --- /dev/null +++ b/docs/extensibility/custom-extensions.md @@ -0,0 +1,24 @@ +# Custom Extensions + +Busola's custom extension feature allows you to design fully custom user interfaces beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays not covered by the built-in components. + +## Getting Started + +To enable the custom extension feature, you must set the corresponding feature flag in your Busola config, which is disabled by default. + +```yaml +EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: true +``` + +## Creating Custom Extensions + +Creating a custom extension is as straightforward as setting up a ConfigMap with the following sections: + +- `data.general`: Contains configuration details +- `data.customHtml`: Defines static HTML content +- `data.customScript`: Adds dynamic behavior to your extension. + +Once your ConfigMap is ready, add it to your cluster, and Busola will load and display your custom UI. + +See this [example](./../../examples/custom-extension/README.md), to learn more. diff --git a/docs/features.md b/docs/features.md index 91283979ae..6878f63f0b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -56,6 +56,15 @@ EXTENSIBILITY: isEnabled: true ``` +- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. See [this example](../examples/custom-extension/README.md). + +Default settings: + +```yaml +EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: false +``` + - **EXTERNAL_NODES** - a list of links to external websites. `category`: a category name, `icon`: an optional icon, `scope`: either `namespace` or `cluster` (defaults to `cluster`), `children`: a list of pairs (label and link). Default settings: diff --git a/examples/custom-extension/README.md b/examples/custom-extension/README.md new file mode 100644 index 0000000000..9f3e7c7c35 --- /dev/null +++ b/examples/custom-extension/README.md @@ -0,0 +1,44 @@ +# Set Up Your Custom Busola Extension + +This example contains a basic custom extension that queries all deployments of a selected namespace of your cluster. Additionally, it retrieves the current weather data for Munich, Germany, from an external weather API. + +To set up and deploy your own custom Busola extension, follow these steps. + +1. Adjust the static HTML content. + +Edit the `ui.html` file to define the static HTML content for your custom extension. + +2. Configure dynamic components. + +Set up dynamic or behavioral components by modifying the custom element defined in the `script.js` file. + +- **Accessing Kubernetes resources**: Use the `fetchWrapper` function to interact with cluster resources through the Kubernetes API. + +- **Making external API requests**: Use the `proxyFetch` function to handle requests to external APIs that are subject to CORS regulations. + +3. Define extension metadata + +Update the `general.yaml` file to define metadata for your custom extension. + +> [! WARNING] +> Ensure that the `general.customElement` property matches the name of the custom element defined in `script.js`. The script is loaded only once, and this property is used to determine whether the custom element is already defined. + +4. Deploy your extension + +Before running the deployment command, ensure that your `kubeconfig` is correctly exported and points to the desired cluster. You can check the current context by running: + +```bash +kubectl config current-context +``` + +Run `./deploy-custom-extension.sh` to create a ConfigMap and deploy it to your cluster + +Alternatively, you can use the following command: + +```bash +kubectl kustomize . | kubectl apply -n kyma-system -f - +``` + +### 5. Test your changes locally + +Run `npm start` to start the development server. diff --git a/examples/custom-extension/deploy-custom-extension.sh b/examples/custom-extension/deploy-custom-extension.sh new file mode 100755 index 0000000000..8861b96b68 --- /dev/null +++ b/examples/custom-extension/deploy-custom-extension.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +kubectl kustomize . > ./custom-ui.yaml +kubectl apply -f ./custom-ui.yaml -n kyma-system diff --git a/examples/custom-extension/general.yaml b/examples/custom-extension/general.yaml new file mode 100644 index 0000000000..fce5e5daeb --- /dev/null +++ b/examples/custom-extension/general.yaml @@ -0,0 +1,10 @@ +resource: + kind: Secret + version: v1 +urlPath: custom-busola-extension-example +category: Kyma +name: Custom busola extension example +scope: cluster +customElement: my-custom-element +description: >- + Custom busola extension example diff --git a/examples/custom-extension/kustomization.yaml b/examples/custom-extension/kustomization.yaml new file mode 100644 index 0000000000..331b64b818 --- /dev/null +++ b/examples/custom-extension/kustomization.yaml @@ -0,0 +1,11 @@ +configMapGenerator: + - name: custom-ui + files: + - customHtml=ui.html + - customScript=script.js + - general=general.yaml + options: + disableNameSuffixHash: true + labels: + busola.io/extension: 'resource' + busola.io/extension-version: '0.5' diff --git a/examples/custom-extension/script.js b/examples/custom-extension/script.js new file mode 100644 index 0000000000..bc3c6bd601 --- /dev/null +++ b/examples/custom-extension/script.js @@ -0,0 +1,220 @@ +function fetchWrapper(url, options = {}) { + if (window.extensionProps?.kymaFetchFn) { + return window.extensionProps.kymaFetchFn(url, options); + } + return fetch(url, options); +} + +function proxyFetch(url, options = {}) { + const baseUrl = window.location.hostname.startsWith('localhost') + ? 'http://localhost:3001/proxy' + : '/proxy'; + const encodedUrl = encodeURIComponent(url); + const proxyUrl = `${baseUrl}?url=${encodedUrl}`; + return fetch(proxyUrl, options); +} + +class MyCustomElement extends HTMLElement { + connectedCallback() { + const shadow = this.attachShadow({ mode: 'open' }); + + // Add basic styling + const style = document.createElement('style'); + style.textContent = ` + .container { + padding: 1rem;lu + } + .deployments-list { + margin-top: 1rem; + } + .deployment-item { + padding: 0.5rem; + margin: 0.5rem 0; + background: #f5f5f5; + border-radius: 4px; + } + .weather-container { + margin-top: 2rem; + padding: 1rem; + background: #e0f7fa; + border-radius: 8px; + } + .weather-item { + padding: 0.5rem 0; + margin: 0.5rem 0; + font-size: 1rem; + } + `; + shadow.appendChild(style); + + // Create container + const container = document.createElement('div'); + container.className = 'container'; + + // Create namespace dropdown + const namespaceSelect = document.createElement('ui5-select'); + namespaceSelect.id = 'namespaceSelect'; + container.appendChild(namespaceSelect); + + // Create deployments container + const deploymentsList = document.createElement('div'); + deploymentsList.className = 'deployments-list'; + container.appendChild(deploymentsList); + + // Create weather container + const weatherContainer = document.createElement('div'); + weatherContainer.className = 'weather-container'; + weatherContainer.id = 'weatherContainer'; + container.appendChild(weatherContainer); + + shadow.appendChild(container); + + // Load initial data + this.loadData(namespaceSelect, deploymentsList); + + // Add change listener + namespaceSelect.addEventListener('change', () => { + this.updateDeploymentsList(namespaceSelect.value, deploymentsList); + }); + + // Fetch and update weather data + fetchMunichWeatherData().then(weatherData => { + this.updateWeatherUI(weatherData, weatherContainer); + }); + } + + async loadData(namespaceSelect, deploymentsList) { + try { + // Get namespaces + const namespaces = await getNamespaces(); + + // Populate namespace dropdown + namespaces.forEach(namespace => { + const option = document.createElement('ui5-option'); + option.value = namespace.metadata.name; + option.innerHTML = namespace.metadata.name; + namespaceSelect.appendChild(option); + }); + + // Load deployments for first namespace + if (namespaces.length > 0) { + this.updateDeploymentsList( + namespaces[0].metadata.name, + deploymentsList, + ); + } + } catch (error) { + console.error('Failed to load data:', error); + } + } + + async updateDeploymentsList(namespace, deploymentsList) { + try { + const deployments = await getDeployments(namespace); + + // Clear current list + deploymentsList.innerHTML = ''; + + // Add deployment to list + deployments.forEach(deployment => { + const deploymentItem = document.createElement('div'); + deploymentItem.className = 'deployment-item'; + deploymentItem.innerHTML = ` +
Name: ${deployment.metadata.name}
+ `; + deploymentsList.appendChild(deploymentItem); + }); + + // Show message if no deployments + if (deployments.length === 0) { + const messageStrip = document.createElement('ui5-message-strip'); + messageStrip.innerHTML = 'No deployments found in this namespace'; + + deploymentsList.innerHTML = messageStrip.outerHTML; + } + } catch (error) { + console.error('Failed to update deployments:', error); + deploymentsList.innerHTML = '
Error loading deployments
'; + } + } + + async updateWeatherUI(weatherData, weatherContainer) { + const { temperature, condition } = weatherData; + weatherContainer.innerHTML = ` + Current weather in Munich: +
Temperature: ${temperature}°C
+
Condition: ${condition}
+ `; + } +} + +async function getNamespaces() { + const resp = await fetchWrapper('/api/v1/namespaces'); + const data = await resp.json(); + return data.items; +} + +async function getDeployments(namespace) { + const resp = await fetchWrapper( + `/apis/apps/v1/namespaces/${namespace}/deployments`, + ); + const data = await resp.json(); + return data.items; +} + +async function fetchMunichWeatherData() { + const latitude = 48.1351; + const longitude = 11.582; + const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true`; + + const response = await proxyFetch(url); + if (!response.ok) { + console.error(`Error fetching weather: ${response.status}`); + return; + } + const data = await response.json(); + + const currentWeather = data.current_weather; + const temperature = currentWeather.temperature; + const weatherCode = currentWeather.weathercode; + + const weatherConditions = { + 0: 'Clear sky', + 1: 'Mainly clear', + 2: 'Partly cloudy', + 3: 'Overcast', + 45: 'Fog', + 48: 'Depositing rime fog', + 51: 'Light drizzle', + 53: 'Moderate drizzle', + 55: 'Dense drizzle', + 56: 'Light freezing drizzle', + 57: 'Dense freezing drizzle', + 61: 'Slight rain', + 63: 'Moderate rain', + 65: 'Heavy rain', + 66: 'Light freezing rain', + 67: 'Heavy freezing rain', + 71: 'Slight snow fall', + 73: 'Moderate snow fall', + 75: 'Heavy snow fall', + 77: 'Snow grains', + 80: 'Slight rain showers', + 81: 'Moderate rain showers', + 82: 'Violent rain showers', + 85: 'Slight snow showers', + 86: 'Heavy snow showers', + 95: 'Thunderstorm', + 96: 'Thunderstorm with slight hail', + 99: 'Thunderstorm with heavy hail', + }; + + const condition = + weatherConditions[weatherCode] || 'Unknown weather condition'; + + return { temperature, condition }; +} + +if (!customElements.get('my-custom-element')) { + customElements.define('my-custom-element', MyCustomElement); +} diff --git a/examples/custom-extension/ui.html b/examples/custom-extension/ui.html new file mode 100644 index 0000000000..7f2d43e838 --- /dev/null +++ b/examples/custom-extension/ui.html @@ -0,0 +1,6 @@ +
+ + Deployments in Namespace + + +
diff --git a/kyma/environments/dev/config.yaml b/kyma/environments/dev/config.yaml index a5651671db..a29e6a3976 100644 --- a/kyma/environments/dev/config.yaml +++ b/kyma/environments/dev/config.yaml @@ -66,6 +66,8 @@ config: isEnabled: true EXTENSIBILITY_INJECTIONS: isEnabled: true + EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: false EXTENSIBILITY_WIZARD: isEnabled: true TRACKING: diff --git a/kyma/environments/prod/config.yaml b/kyma/environments/prod/config.yaml index 6c570ccd12..2b239ac776 100644 --- a/kyma/environments/prod/config.yaml +++ b/kyma/environments/prod/config.yaml @@ -68,6 +68,8 @@ config: isEnabled: true EXTENSIBILITY_INJECTIONS: isEnabled: true + EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: false EXTENSIBILITY_WIZARD: isEnabled: true EVENTING: diff --git a/kyma/environments/stage/config.yaml b/kyma/environments/stage/config.yaml index 8de69d54aa..a1bf90088b 100644 --- a/kyma/environments/stage/config.yaml +++ b/kyma/environments/stage/config.yaml @@ -66,6 +66,8 @@ config: isEnabled: true EXTENSIBILITY_INJECTIONS: isEnabled: true + EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: false EXTENSIBILITY_WIZARD: isEnabled: true EVENTING: diff --git a/public/defaultConfig.yaml b/public/defaultConfig.yaml index 15c3504ba3..a59bdaad55 100644 --- a/public/defaultConfig.yaml +++ b/public/defaultConfig.yaml @@ -49,6 +49,8 @@ config: isEnabled: true EXTENSIBILITY_INJECTIONS: isEnabled: true + EXTENSIBILITY_CUSTOM_COMPONENTS: + isEnabled: false EXTENSIBILITY_WIZARD: isEnabled: true TRACKING: diff --git a/src/components/Extensibility/ExtensibilityList.js b/src/components/Extensibility/ExtensibilityList.js index 3c1bb5024b..4a7e9ec9bf 100644 --- a/src/components/Extensibility/ExtensibilityList.js +++ b/src/components/Extensibility/ExtensibilityList.js @@ -1,5 +1,6 @@ import pluralize from 'pluralize'; import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; import { ResourcesList } from 'shared/components/ResourcesList/ResourcesList'; import { usePrepareListProps } from 'resources/helpers'; @@ -22,6 +23,9 @@ import { sortBy } from './helpers/sortBy'; import { Widget } from './components/Widget'; import { DataSourcesContextProvider } from './contexts/DataSources'; import { useJsonata } from './hooks/useJsonata'; +import { useFeature } from 'hooks/useFeature'; +import { createPortal } from 'react-dom'; +import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog'; export const ExtensibilityListCore = ({ resMetaData, @@ -140,6 +144,33 @@ const ExtensibilityList = ({ overrideResMetadata, ...props }) => { const defaultResMetadata = useGetCRbyPath(); const resMetaData = overrideResMetadata || defaultResMetadata; const { urlPath, defaultPlaceholder } = resMetaData?.general ?? {}; + const { isEnabled: isExtensibilityCustomComponentsEnabled } = useFeature( + 'EXTENSIBILITY_CUSTOM_COMPONENTS', + ); + + useEffect(() => { + const customElement = resMetaData?.general?.customElement; + const customScript = resMetaData?.customScript; + + if ( + isExtensibilityCustomComponentsEnabled && + customElement && + customScript && + !customElements.get(customElement) + ) { + const script = document.createElement('script'); + script.type = 'module'; + script.textContent = customScript; + script.onerror = e => { + console.error('Script loading or execution error:', e); + }; + document.head.appendChild(script); + + return () => { + document.head.removeChild(script); + }; + } + }, [resMetaData, isExtensibilityCustomComponentsEnabled]); return ( { > - + {isExtensibilityCustomComponentsEnabled && resMetaData.customHtml ? ( + <> +
+ {createPortal(, document.body)} + + ) : ( + + )}
diff --git a/src/state/navigation/extensionsAtom.ts b/src/state/navigation/extensionsAtom.ts index 591e35701d..426102e79e 100644 --- a/src/state/navigation/extensionsAtom.ts +++ b/src/state/navigation/extensionsAtom.ts @@ -45,6 +45,8 @@ type ConfigMapData = { dataSources: string; translations: string; presets: string; + customScript: string; + customHtml: string; }; type ConfigMapResponse = K8sResource & { @@ -61,6 +63,10 @@ type ConfigMapListResponse = } | undefined; +interface ExtensionProps { + kymaFetchFn: (url: string, options?: any) => Promise; +} + const isTheSameNameAndUrl = ( firstCM: Partial, secondCM: Partial, @@ -288,6 +294,7 @@ const getExtensions = async ( kubeconfigNamespace = 'kube-public', currentNamespace: string, permissionSet: PermissionSetState, + extCustomComponentsEnabled: boolean, ) => { if (!fetchFn) { return null; @@ -324,6 +331,12 @@ const getExtensions = async ( ) as ExtResource, }; + if (extCustomComponentsEnabled) { + extResourceWithMetadata.data.customHtml = + currentConfigMap.data.customHtml || ''; + extResourceWithMetadata.data.customScript = + currentConfigMap.data.customScript || ''; + } if (!extResourceWithMetadata.data) return accumulator; const indexOfTheSameExtension = accumulator.findIndex(ext => @@ -406,10 +419,43 @@ export const useGetExtensions = () => { const { isEnabled: isExtensibilityWizardEnabled } = useFeature( 'EXTENSIBILITY_WIZARD', ); + const { isEnabled: isExtensibilityCustomComponentsEnabled } = useFeature( + 'EXTENSIBILITY_CUSTOM_COMPONENTS', + ); const { data: crds } = useGet( `/apis/apiextensions.k8s.io/v1/customresourcedefinitions`, ); + useEffect(() => { + if (isExtensibilityCustomComponentsEnabled) { + // Wrap busola fetch function to be able to use it in the extensions as regular fetch. + // It reduces the learning curve for the extension developers and introduces loose coupling between Busola and the extensions. + function asRegularFetch(busolaFetch: FetchFn, url: string, options: any) { + return busolaFetch({ + relativeUrl: url, + init: options, + abortController: options?.signal + ? { signal: options?.signal, abort: () => {} } + : undefined, + }); + } + + if (fetchFn) { + (window as Window & { + extensionProps?: ExtensionProps; + }).extensionProps = { + kymaFetchFn: (url: string, options: any) => + asRegularFetch(fetchFn, url, options), + }; + } + } + + return () => { + delete (window as Window & { extensionProps?: ExtensionProps }) + .extensionProps; + }; + }, [fetchFn, auth, isExtensibilityCustomComponentsEnabled]); + useEffect(() => { (crds as any)?.items.forEach((crd: CustomResourceDefinition) => { RESOURCE_PATH[crd?.spec.names.kind as keyof typeof RESOURCE_PATH] = @@ -433,6 +479,7 @@ export const useGetExtensions = () => { cluster.currentContext.namespace || 'kube-public', namespace, permissionSet, + isExtensibilityCustomComponentsEnabled ?? false, ); const statics = await getStatics( diff --git a/src/state/types.ts b/src/state/types.ts index 453b9ae06d..3d5341721f 100644 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -22,6 +22,7 @@ export const configFeaturesNames = { VISUAL_RESOURCES: 'VISUAL_RESOURCES', EXTENSIBILITY: 'EXTENSIBILITY', EXTENSIBILITY_INJECTIONS: 'EXTENSIBILITY_INJECTIONS', + EXTENSIBILITY_CUSTOM_COMPONENTS: 'EXTENSIBILITY_CUSTOM_COMPONENTS', EXTENSIBILITY_WIZARD: 'EXTENSIBILITY_WIZARD', TRACKING: 'TRACKING', PROTECTED_RESOURCES: 'PROTECTED_RESOURCES', @@ -98,6 +99,8 @@ export type ExtResource = { presets: any[]; dataSources: Record; injections?: ExtInjection[]; + customHtml: {}; + customScript: {}; }; export type ExtensibilityNodesExt = {