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 = ` +