From 20f3c84cd01354b8e128b424fc5da99dce758df1 Mon Sep 17 00:00:00 2001 From: chriskari Date: Fri, 6 Dec 2024 12:01:45 +0100 Subject: [PATCH 01/10] feat: add proxyhandler to backend --- backend/common.js | 10 ++++++---- backend/index.js | 7 ++++++- backend/proxy.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 backend/proxy.js 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 }; From 7b4298825e645d789548f818da56e15671f58822 Mon Sep 17 00:00:00 2001 From: chriskari Date: Fri, 6 Dec 2024 12:22:07 +0100 Subject: [PATCH 02/10] feat: adjust busola code to handle custom extensions --- 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 | 37 ++++++++++++++- src/state/navigation/extensionsAtom.ts | 45 ++++++++++++++++++- src/state/types.ts | 3 ++ 7 files changed, 91 insertions(+), 2 deletions(-) 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 5a2d402e1c..6de33f52eb 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,7 @@ 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'; export const ExtensibilityListCore = ({ resMetaData, @@ -142,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 ? ( +
+ ) : ( + + )}
diff --git a/src/state/navigation/extensionsAtom.ts b/src/state/navigation/extensionsAtom.ts index 591e35701d..1c7ce81c56 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 & { @@ -146,6 +148,7 @@ const getExtensionWizards = async ( kubeconfigNamespace = 'kube-public', currentNamespace: string, permissionSet: PermissionSetState, + extCustomComponentsEnabled: boolean, ) => { if (!fetchFn) { return null; @@ -177,7 +180,10 @@ const getExtensionWizards = async ( convertYamlToObject, ) as ExtResource, }; - + if (extCustomComponentsEnabled) { + extResourceWithMetadata.data.customHtml = + extResourceWithMetadata.data.customHtml || ''; + } if (!extResourceWithMetadata.data) return accumulator; const indexOfTheSameExtension = accumulator.findIndex(ext => @@ -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,38 @@ 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: any, url: string, options: any) { + return busolaFetch({ + relativeUrl: url, + init: options, + abortController: options?.signal + ? { signal: options?.signal } + : undefined, + }); + } + + (window as any).extensionProps = { + kymaFetchFn: (url: string, options: any) => + asRegularFetch(fetchFn, url, options), + }; + } + + return () => { + delete (window as any).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 +474,7 @@ export const useGetExtensions = () => { cluster.currentContext.namespace || 'kube-public', namespace, permissionSet, + isExtensibilityCustomComponentsEnabled ?? false, ); const statics = await getStatics( @@ -447,6 +489,7 @@ export const useGetExtensions = () => { cluster.currentContext.namespace || 'kube-public', namespace, permissionSet, + isExtensibilityCustomComponentsEnabled ?? false, ); if (!wizardConfigs || !isExtensibilityWizardEnabled) { 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 = { From 881b959361ae8b77874987dc3ee388fb712ad5cd Mon Sep 17 00:00:00 2001 From: chriskari Date: Fri, 6 Dec 2024 12:25:34 +0100 Subject: [PATCH 03/10] feat: added example custom extension --- examples/web-component-ext/README.md | 47 ++++ examples/web-component-ext/deploy.sh | 4 + examples/web-component-ext/general.yaml | 10 + examples/web-component-ext/kustomization.yaml | 11 + examples/web-component-ext/script.js | 220 ++++++++++++++++++ examples/web-component-ext/ui.html | 6 + .../Extensibility/ExtensibilityList.js | 11 +- 7 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 examples/web-component-ext/README.md create mode 100755 examples/web-component-ext/deploy.sh create mode 100644 examples/web-component-ext/general.yaml create mode 100644 examples/web-component-ext/kustomization.yaml create mode 100644 examples/web-component-ext/script.js create mode 100644 examples/web-component-ext/ui.html diff --git a/examples/web-component-ext/README.md b/examples/web-component-ext/README.md new file mode 100644 index 0000000000..326973e8a5 --- /dev/null +++ b/examples/web-component-ext/README.md @@ -0,0 +1,47 @@ +# Set Up Your Custom Busola Extension + +This example contains a basic custom extension, that queries all deployments corresponding to a selected namespace of your cluster, and additionally 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 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. + +#### ⚠️ Important: + +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 + +Run `./deploy.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/web-component-ext/deploy.sh b/examples/web-component-ext/deploy.sh new file mode 100755 index 0000000000..8861b96b68 --- /dev/null +++ b/examples/web-component-ext/deploy.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/web-component-ext/general.yaml b/examples/web-component-ext/general.yaml new file mode 100644 index 0000000000..fce5e5daeb --- /dev/null +++ b/examples/web-component-ext/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/web-component-ext/kustomization.yaml b/examples/web-component-ext/kustomization.yaml new file mode 100644 index 0000000000..331b64b818 --- /dev/null +++ b/examples/web-component-ext/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/web-component-ext/script.js b/examples/web-component-ext/script.js new file mode 100644 index 0000000000..bc3c6bd601 --- /dev/null +++ b/examples/web-component-ext/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/web-component-ext/ui.html b/examples/web-component-ext/ui.html new file mode 100644 index 0000000000..7f2d43e838 --- /dev/null +++ b/examples/web-component-ext/ui.html @@ -0,0 +1,6 @@ +
+ + Deployments in Namespace + + +
diff --git a/src/components/Extensibility/ExtensibilityList.js b/src/components/Extensibility/ExtensibilityList.js index 6de33f52eb..34141a1ea6 100644 --- a/src/components/Extensibility/ExtensibilityList.js +++ b/src/components/Extensibility/ExtensibilityList.js @@ -24,6 +24,8 @@ 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, @@ -182,9 +184,12 @@ const ExtensibilityList = ({ overrideResMetadata, ...props }) => { {isExtensibilityCustomComponentsEnabled && resMetaData.customHtml ? ( -
+ <> +
+ {createPortal(, document.body)} + ) : ( )} From 1c122531b3d5ed5105bb1b4fdd2e9b30a620f74d Mon Sep 17 00:00:00 2001 From: chriskari Date: Fri, 6 Dec 2024 12:37:50 +0100 Subject: [PATCH 04/10] feat: rename folder --- examples/{web-component-ext => custom-extension}/README.md | 0 examples/{web-component-ext => custom-extension}/deploy.sh | 0 examples/{web-component-ext => custom-extension}/general.yaml | 0 .../{web-component-ext => custom-extension}/kustomization.yaml | 0 examples/{web-component-ext => custom-extension}/script.js | 0 examples/{web-component-ext => custom-extension}/ui.html | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename examples/{web-component-ext => custom-extension}/README.md (100%) rename examples/{web-component-ext => custom-extension}/deploy.sh (100%) rename examples/{web-component-ext => custom-extension}/general.yaml (100%) rename examples/{web-component-ext => custom-extension}/kustomization.yaml (100%) rename examples/{web-component-ext => custom-extension}/script.js (100%) rename examples/{web-component-ext => custom-extension}/ui.html (100%) diff --git a/examples/web-component-ext/README.md b/examples/custom-extension/README.md similarity index 100% rename from examples/web-component-ext/README.md rename to examples/custom-extension/README.md diff --git a/examples/web-component-ext/deploy.sh b/examples/custom-extension/deploy.sh similarity index 100% rename from examples/web-component-ext/deploy.sh rename to examples/custom-extension/deploy.sh diff --git a/examples/web-component-ext/general.yaml b/examples/custom-extension/general.yaml similarity index 100% rename from examples/web-component-ext/general.yaml rename to examples/custom-extension/general.yaml diff --git a/examples/web-component-ext/kustomization.yaml b/examples/custom-extension/kustomization.yaml similarity index 100% rename from examples/web-component-ext/kustomization.yaml rename to examples/custom-extension/kustomization.yaml diff --git a/examples/web-component-ext/script.js b/examples/custom-extension/script.js similarity index 100% rename from examples/web-component-ext/script.js rename to examples/custom-extension/script.js diff --git a/examples/web-component-ext/ui.html b/examples/custom-extension/ui.html similarity index 100% rename from examples/web-component-ext/ui.html rename to examples/custom-extension/ui.html From ce0d50bebe4066dd8e8b4b824b90b849f04fb33e Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 9 Dec 2024 09:59:43 +0100 Subject: [PATCH 05/10] feat: add docs --- docs/extensibility/README.md | 2 ++ docs/extensibility/custom-extensions.md | 24 ++++++++++++++++++++++++ docs/features.md | 9 +++++++++ examples/custom-extension/README.md | 2 +- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 docs/extensibility/custom-extensions.md 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..672170056e --- /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 that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the standard built-in components. + +## Getting Started + +First, to enable the custom extension feature you need to 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. + +The best way to get familiar with this mechanism is to have a look at our [example](./../../examples/custom-extension/README.md), where everything is explained in detail. diff --git a/docs/features.md b/docs/features.md index 91283979ae..2b2ed27cba 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. An example for a custom extension can be found [here](../examples/web-component-ext/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 index 326973e8a5..6a6242c06e 100644 --- a/examples/custom-extension/README.md +++ b/examples/custom-extension/README.md @@ -1,6 +1,6 @@ # Set Up Your Custom Busola Extension -This example contains a basic custom extension, that queries all deployments corresponding to a selected namespace of your cluster, and additionally retrieves the current weather data for Munich, Germany from an external weather API. +This example contains a basic custom extension, that queries all deployments of a selected namespace of your cluster, and additionally 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. From c73668f162d61abcafa38356341214a14f2bf494 Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 9 Dec 2024 10:02:42 +0100 Subject: [PATCH 06/10] feat: update docs --- docs/extensibility/custom-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensibility/custom-extensions.md b/docs/extensibility/custom-extensions.md index 672170056e..a2c4048f40 100644 --- a/docs/extensibility/custom-extensions.md +++ b/docs/extensibility/custom-extensions.md @@ -1,6 +1,6 @@ # Custom Extensions -Busola's custom extension feature allows you to design fully custom user interfaces that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the standard built-in components. +Busola's custom extension feature allows you to design fully custom user interfaces that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the built-in components. ## Getting Started From ee5d4eb9cd3f9cd158276e99287931348434b5a9 Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 9 Dec 2024 10:07:28 +0100 Subject: [PATCH 07/10] feat: update docs --- docs/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.md b/docs/features.md index 2b2ed27cba..6cda813fec 100644 --- a/docs/features.md +++ b/docs/features.md @@ -56,7 +56,7 @@ EXTENSIBILITY: isEnabled: true ``` -- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. An example for a custom extension can be found [here](../examples/web-component-ext/README.md). +- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. An example for a custom extension can be found [here](../examples/custom-extension/README.md). Default settings: From 7a52a135f2ea38370f8e8717e0789bf453d6b16a Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 9 Dec 2024 11:35:41 +0100 Subject: [PATCH 08/10] feat: add more specific types --- examples/custom-extension/README.md | 2 +- .../{deploy.sh => deploy-custom-extension.sh} | 0 src/state/navigation/extensionsAtom.ts | 23 +++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) rename examples/custom-extension/{deploy.sh => deploy-custom-extension.sh} (100%) diff --git a/examples/custom-extension/README.md b/examples/custom-extension/README.md index 6a6242c06e..eb94abbf8a 100644 --- a/examples/custom-extension/README.md +++ b/examples/custom-extension/README.md @@ -32,7 +32,7 @@ Ensure that the `general.customElement` property matches the name of the custom ### 4. Deploy Your Extension -Run `./deploy.sh` to create a ConfigMap and deploy it to your cluster +Run `./deploy-custom-extension.sh` to create a ConfigMap and deploy it to your cluster Alternatively, you can use the following command: diff --git a/examples/custom-extension/deploy.sh b/examples/custom-extension/deploy-custom-extension.sh similarity index 100% rename from examples/custom-extension/deploy.sh rename to examples/custom-extension/deploy-custom-extension.sh diff --git a/src/state/navigation/extensionsAtom.ts b/src/state/navigation/extensionsAtom.ts index 1c7ce81c56..95dd5f4dc1 100644 --- a/src/state/navigation/extensionsAtom.ts +++ b/src/state/navigation/extensionsAtom.ts @@ -63,6 +63,10 @@ type ConfigMapListResponse = } | undefined; +interface ExtensionProps { + kymaFetchFn: (url: string, options?: any) => Promise; +} + const isTheSameNameAndUrl = ( firstCM: Partial, secondCM: Partial, @@ -430,24 +434,29 @@ export const useGetExtensions = () => { 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: any, url: string, options: any) { + function asRegularFetch(busolaFetch: FetchFn, url: string, options: any) { return busolaFetch({ relativeUrl: url, init: options, abortController: options?.signal - ? { signal: options?.signal } + ? { signal: options?.signal, abort: () => {} } : undefined, }); } - (window as any).extensionProps = { - kymaFetchFn: (url: string, options: any) => - asRegularFetch(fetchFn, url, options), - }; + if (fetchFn) { + (window as Window & { + extensionProps?: ExtensionProps; + }).extensionProps = { + kymaFetchFn: (url: string, options: any) => + asRegularFetch(fetchFn, url, options), + }; + } } return () => { - delete (window as any).extensionProps; + delete (window as Window & { extensionProps?: ExtensionProps }) + .extensionProps; }; }, [fetchFn, auth, isExtensibilityCustomComponentsEnabled]); From d3a05e9f40d9f57462b4336d1e8d069d2235e980 Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 9 Dec 2024 13:05:11 +0100 Subject: [PATCH 09/10] feat: adjust to review comments --- examples/custom-extension/README.md | 6 ++++++ src/state/navigation/extensionsAtom.ts | 11 +++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/custom-extension/README.md b/examples/custom-extension/README.md index eb94abbf8a..ac8785b158 100644 --- a/examples/custom-extension/README.md +++ b/examples/custom-extension/README.md @@ -32,6 +32,12 @@ Ensure that the `general.customElement` property matches the name of the custom ### 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: diff --git a/src/state/navigation/extensionsAtom.ts b/src/state/navigation/extensionsAtom.ts index 95dd5f4dc1..426102e79e 100644 --- a/src/state/navigation/extensionsAtom.ts +++ b/src/state/navigation/extensionsAtom.ts @@ -152,7 +152,6 @@ const getExtensionWizards = async ( kubeconfigNamespace = 'kube-public', currentNamespace: string, permissionSet: PermissionSetState, - extCustomComponentsEnabled: boolean, ) => { if (!fetchFn) { return null; @@ -184,10 +183,7 @@ const getExtensionWizards = async ( convertYamlToObject, ) as ExtResource, }; - if (extCustomComponentsEnabled) { - extResourceWithMetadata.data.customHtml = - extResourceWithMetadata.data.customHtml || ''; - } + if (!extResourceWithMetadata.data) return accumulator; const indexOfTheSameExtension = accumulator.findIndex(ext => @@ -337,9 +333,9 @@ const getExtensions = async ( if (extCustomComponentsEnabled) { extResourceWithMetadata.data.customHtml = - currentConfigMap.data.customHtml; + currentConfigMap.data.customHtml || ''; extResourceWithMetadata.data.customScript = - currentConfigMap.data.customScript; + currentConfigMap.data.customScript || ''; } if (!extResourceWithMetadata.data) return accumulator; @@ -498,7 +494,6 @@ export const useGetExtensions = () => { cluster.currentContext.namespace || 'kube-public', namespace, permissionSet, - isExtensibilityCustomComponentsEnabled ?? false, ); if (!wizardConfigs || !isExtensibilityWizardEnabled) { From 152dc7d9bc21d12e881f3a3517d6b33cec79286d Mon Sep 17 00:00:00 2001 From: chriskari Date: Wed, 11 Dec 2024 13:31:16 +0100 Subject: [PATCH 10/10] adjust to comments --- docs/extensibility/custom-extensions.md | 8 +++---- docs/features.md | 2 +- examples/custom-extension/README.md | 31 +++++++++---------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/docs/extensibility/custom-extensions.md b/docs/extensibility/custom-extensions.md index a2c4048f40..ef645b8a57 100644 --- a/docs/extensibility/custom-extensions.md +++ b/docs/extensibility/custom-extensions.md @@ -1,17 +1,17 @@ # Custom Extensions -Busola's custom extension feature allows you to design fully custom user interfaces that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the built-in components. +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 -First, to enable the custom extension feature you need to set the corresponding feature flag in your busola config, which is disabled by default. +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 Custom Extensions Creating a custom extension is as straightforward as setting up a ConfigMap with the following sections: @@ -21,4 +21,4 @@ Creating a custom extension is as straightforward as setting up a ConfigMap with Once your ConfigMap is ready, add it to your cluster, and Busola will load and display your custom UI. -The best way to get familiar with this mechanism is to have a look at our [example](./../../examples/custom-extension/README.md), where everything is explained in detail. +See this [example](./../../examples/custom-extension/README.md), to learn more. diff --git a/docs/features.md b/docs/features.md index 6cda813fec..6878f63f0b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -56,7 +56,7 @@ EXTENSIBILITY: isEnabled: true ``` -- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. An example for a custom extension can be found [here](../examples/custom-extension/README.md). +- **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: diff --git a/examples/custom-extension/README.md b/examples/custom-extension/README.md index ac8785b158..9f3e7c7c35 100644 --- a/examples/custom-extension/README.md +++ b/examples/custom-extension/README.md @@ -1,38 +1,31 @@ # Set Up Your Custom Busola Extension -This example contains a basic custom extension, that queries all deployments of a selected namespace of your cluster, and additionally retrieves the current weather data for Munich, Germany from an external weather API. +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 Static HTML Content +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 +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. +- **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 +3. Define extension metadata Update the `general.yaml` file to define metadata for your custom extension. -#### ⚠️ Important: - -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. +> [! 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 -### 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: +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 @@ -46,8 +39,6 @@ Alternatively, you can use the following command: kubectl kustomize . | kubectl apply -n kyma-system -f - ``` ---- - -### 5. Test Your Changes Locally +### 5. Test your changes locally Run `npm start` to start the development server.