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)} + ) : ( )}