Skip to content

Commit

Permalink
feat: added example custom extension
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskari committed Dec 6, 2024
1 parent 7b42988 commit 881b959
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 3 deletions.
47 changes: 47 additions & 0 deletions examples/web-component-ext/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions examples/web-component-ext/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

kubectl kustomize . > ./custom-ui.yaml
kubectl apply -f ./custom-ui.yaml -n kyma-system
10 changes: 10 additions & 0 deletions examples/web-component-ext/general.yaml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions examples/web-component-ext/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -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'
220 changes: 220 additions & 0 deletions examples/web-component-ext/script.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div><strong>Name:</strong> ${deployment.metadata.name}</div>
`;
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 = '<div>Error loading deployments</div>';
}
}

async updateWeatherUI(weatherData, weatherContainer) {
const { temperature, condition } = weatherData;
weatherContainer.innerHTML = `
<ui5-title level="H4">Current weather in Munich:</ui5-title>
<div class="weather-item"><strong>Temperature:</strong> ${temperature}°C</div>
<div class="weather-item"><strong>Condition:</strong> ${condition}</div>
`;
}
}

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}&current_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);
}
6 changes: 6 additions & 0 deletions examples/web-component-ext/ui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<ui5-panel header-text="Custom busola extension example">
<ui5-title level="H4">Deployments in Namespace</ui5-title>
<my-custom-element></my-custom-element>
</ui5-panel>
</div>
11 changes: 8 additions & 3 deletions src/components/Extensibility/ExtensibilityList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -182,9 +184,12 @@ const ExtensibilityList = ({ overrideResMetadata, ...props }) => {
<DataSourcesContextProvider dataSources={resMetaData?.dataSources || {}}>
<ExtensibilityErrBoundary key={urlPath}>
{isExtensibilityCustomComponentsEnabled && resMetaData.customHtml ? (
<div
dangerouslySetInnerHTML={{ __html: resMetaData.customHtml }}
></div>
<>
<div
dangerouslySetInnerHTML={{ __html: resMetaData.customHtml }}
></div>
{createPortal(<YamlUploadDialog />, document.body)}
</>
) : (
<ExtensibilityListCore resMetaData={resMetaData} {...props} />
)}
Expand Down

0 comments on commit 881b959

Please sign in to comment.