Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/custom-extensions/assets/DynamicPage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/custom-extensions/assets/MonacoEditor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions docs/custom-extensions/busola-web-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Custom Busola Web Components

Busola provides a suite of custom web components to enhance your application's functionality.

## Utility Functions

All custom web components expose methods to dynamically update their properties, attributes, and slots after initialization.
You can pass the string and boolean properties as standard HTML attributes. For example:

```HTML
<monaco-editor read-only="true"></monaco-editor>
```

You can pass functions, objects, and arrays using the `setProp` function. For example:

```JS
const editor = document.querySelector('monaco-editor');
editor.setProp('on-change', (value) => console.log('New content:', value));
```

You can pass HTML elements using the `setSlot` attribute. For example:

```JS
const dynamicPage = document.querySelector('dynamic-page-component');
const customFooter = document.createElement('div');
customFooter.textContent = 'Custom Footer Content';
dynamicPage.setSlot('footer', customFooter);
```

## Custom Web Components

- [Monaco Editor](#monaco-editor)
- [Dynamic Page](#dynamic-page)

### Monaco Editor

The Monaco Editor component is a versatile code editor. It provides features such as syntax highlighting and autocompletion. It supports the following attributes and properties. Attributes correspond to camel-cased React props when accessed programmatically.

| Parameter | Required | Type | Description |
| --------------------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **value** | No | string | The initial code content displayed in the editor. Defaults to an empty string. |
| **placeholder** | No | string | Specifies a short hint about the input field value. |
| **language** | No | string | Specifies the programming language of the editor's content (for example, `javascript`, `json`). Defaults to `javascript`. |
| **height** | No | string | Specifies the height of the component. Must include the unit (e.g., `100px`, `50vh`). |
| **schema-id** | No | string | A unique identifier for the JSON schema used to enable autocompletion and validation. If not provided, autocompletion is disabled. |
| **autocompletion-disabled** | No | boolean | Disables autocompletion suggestions when set to `true`. |
| **read-only** | No | boolean | Specifies if the field is read-only. Defaults to `false`. |
| **on-change** | No | function | Callback function triggered when the content changes. |
| **on-mount** | No | function | Callback function triggered when the editor mounts. |
| **on-blur** | No | function | Callback function triggered when the editor loses focus. |
| **on-focus** | No | function | Callback function triggered when the editor gains focus. |
| **update-value-on-parent-change** | No | boolean | Updates the editor content if the parent component changes its `value` prop. |
| **options** | No | object | Custom options for configuring the Monaco Editor. Refer to the [Monaco Editor API](https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html) for available options. |
| **error** | No | string | Displays an error message in the editor when provided. If an error is displayed, it indicates that the current input is invalid, but the previous valid input will be used by the editor. |

See the following example:

```HTML
<monaco-editor
value="console.log('Hello!')"
language="javascript"
height="200px"
placeholder="Write some code..."
></monaco-editor>
```

<img src="./assets/MonacoEditor.png" alt="Example of a Monaco Editor" width="70%" style="border: 1px solid #D2D5D9">

### Dynamic Page

The Dynamic Page web component displays content on the page, consisting of a title, a header, a content area, an optional inline edit form, and a floating footer. It supports the following attributes and properties. Attributes correspond to camel-cased React props when accessed programmatically.

| Parameter | Required | Type | Description |
| ------------------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **title** | No | string | The title of the page displayed in the header. |
| **description** | No | string | A description displayed below the title. |
| **actions** | No | node | Custom actions rendered in the header toolbar. |
| **children** | No | node | Child elements or components to be rendered within the page. |
| **column-wrapper-class-name** | No | string | Additional class names for the column wrapper, used for styling purposes. |
| **content** | No | node | Content displayed in the main section of the page. |
| **footer** | No | node | Content displayed in the footer section. |
| **layout-number** | No | string | Layout identifier for column management. |
| **layout-close-url** | No | string | URL to navigate to when the column layout is closed. |
| **inline-edit-form** | No | function | A function defining the inline edit form. It receives the `stickyHeaderHeight` as an argument and is expected to return a HTML element. |
| **show-yaml-tab** | No | boolean | Specifies whether to show a YAML editing tab. |
| **protected-resource** | No | boolean | Indicates whether the resource is protected. |
| **protected-resource-warning** | No | node | Warning message for protected resources. |
| **class-name** | No | string | Additional class names for the component, used for custom styling. |
| **custom-action-if-form-open** | No | function | Specifies a custom action triggered when a user tries to navigate out of the Edit form tab. It receives four arguments: `isResourceEdited`, `setIsResourceEdited`, `isFormOpen`, `setIsFormOpen`. |

#### `custom-action-if-form-open`

The `custom-action-if-form-open` prop in the Dynamic Page component is a customizable callback function designed to handle specific actions when a form is open. It receives four arguments:

| Argument | Type | Description |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **isResourceEdited** | object | Indicates if the current resource has been edited. The object has the structure: `{ isEdited: boolean; discardAction?: Function; }` |
| **setIsResourceEdited** | function | A state setter function to update the `isResourceEdited` state. |
| **isFormOpen** | object | Tracks the status of the inline edit form. The object has the structure: `{ formOpen: boolean; leavingForm: boolean; }` |
| **setIsFormOpen** | function | A state setter function to update the `isFormOpen` state. |

See the following example:

```HTML
<dynamic-page-component
title="Sample Page"
description="This is a dynamic page."
show-yaml-tab="true"
class-name="custom-page-class"
>
```

<img src="./assets/DynamicPage.png" alt="Example of a Monaco Editor" width="50%" style="border: 1px solid #D2D5D9">

To see an exemplary configuration of the Busola custom extension feature using web components, check files in [this](examples/../../../examples/busola-web-components/README.md) example.
21 changes: 21 additions & 0 deletions examples/busola-web-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Busola Web Components Example

This example demonstrates the use of custom web components, including the Dynamic Page and Monaco Editor. It showcases how to set attributes, properties and manage content.

## Prerequisites

Before you begin, ensure you have custom extensions enabled in your cluster:

```
kubectl apply -f busola-config.yaml
```

## Set Up Your Custom Busola Extension

To apply this example in your cluster execute:

```bash
kubectl kustomize . | kubectl apply -n kyma-system -f -
```

For more information follow [this]('examples/../../custom-extension/README.md) documentation.
11 changes: 11 additions & 0 deletions examples/busola-web-components/busola-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: busola-config
namespace: kube-public
data:
config: |
config:
features:
EXTENSIBILITY_CUSTOM_COMPONENTS:
isEnabled: true
10 changes: 10 additions & 0 deletions examples/busola-web-components/general.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource:
kind: Secret
version: v1
urlPath: busola-web-components-example
category: Kyma
name: Busola Web Components example
scope: cluster
customElement: my-custom-element
description: >-
Busola Web Components example
11 changes: 11 additions & 0 deletions examples/busola-web-components/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
configMapGenerator:
- name: web-components-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'
114 changes: 114 additions & 0 deletions examples/busola-web-components/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
class MyCustomPage extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });

// Add basic styling
const style = document.createElement('style');
style.textContent = `
.edit-form-inline, #monaco-editor-panel {
margin: 1rem;
}
`;
shadow.appendChild(style);

// Create container
const container = document.createElement('div');
container.className = 'container';
shadow.appendChild(container);

// Create Dynamic Page
const dynamicPage = this.createDynamicPage();
container.appendChild(dynamicPage);
dynamicPage.setAttribute('title', 'Dynamic Page');
}

createDynamicPage() {
const dynamicPageComponent = document.createElement(
'dynamic-page-component',
);
dynamicPageComponent.setAttribute('title', 'Dynamic Page');
dynamicPageComponent.classList.add('dynamic-page');

// Set inlineEditForm function
dynamicPageComponent.setProp('inline-edit-form', this.renderEditForm);
// Set custom action when form is open
dynamicPageComponent.setProp(
'custom-action-if-form-open',
this.handleActionIfFormOpen,
);

// Set Dynamic Page content
dynamicPageComponent.setSlot('content', this.createMonacoEditor());

return dynamicPageComponent;
}

// Create Monaco Editor
createMonacoEditor() {
const monacoEditorPanel = document.createElement('ui5-panel');
monacoEditorPanel.setAttribute('id', 'monaco-editor-panel');
monacoEditorPanel.setAttribute('header-text', 'Monaco editor example');

const monacoEditor = document.createElement('monaco-editor');
monacoEditor.classList.add('monaco');
monacoEditor.setAttribute('value', '');
monacoEditor.setAttribute('language', 'javascript');
monacoEditor.setAttribute('height', '200px');
monacoEditor.setAttribute('placeholder', 'Write something!');
monacoEditor.setProp('on-change', value => {
this.setAttribute('value', value);
});

monacoEditorPanel.appendChild(monacoEditor);
return monacoEditorPanel;
}

// inlineEditForm function
renderEditForm() {
const form = document.createElement('form');
form.classList.add('edit-form-inline');

const namePanel = document.createElement('ui5-panel');
namePanel.setAttribute('header-text', 'Name');

const nameLabel = document.createElement('ui5-label');
nameLabel.setAttribute('for', 'name');
nameLabel.textContent = 'Name:';
namePanel.appendChild(nameLabel);

const nameInput = document.createElement('ui5-input');
nameInput.classList.add('name-input');
nameInput.setAttribute('id', 'name');
nameInput.setAttribute('name', 'name');
nameInput.setAttribute('value', 'John Doe');
nameInput.setAttribute('placeholder', 'Enter your name');
nameInput.addEventListener('input', event => {
console.log('Change!');
});
namePanel.appendChild(nameInput);

const submitButton = document.createElement('ui5-button');
submitButton.textContent = 'Submit';
submitButton.setAttribute('type', 'submit');
submitButton.addEventListener('click', event => {
event.preventDefault();
console.log('Submit!');
});
form.appendChild(namePanel);
form.appendChild(submitButton);

return form;
}

handleActionIfFormOpen(
isResourceEdited,
setIsResourceEdited,
isFormOpen,
setIsFormOpen,
) {
setIsFormOpen({ formOpen: false, leavingForm: false });
}
}

// Define the custom element
customElements.define('my-custom-page', MyCustomPage);
3 changes: 3 additions & 0 deletions examples/busola-web-components/ui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<my-custom-page></my-custom-page>
</div>
2 changes: 2 additions & 0 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import useSidebarCondensed from 'sidebar/useSidebarCondensed';
import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom';
import { useGetKymaResources } from 'state/kymaResourcesAtom';

import '../../web-components/index'; //Import for custom Web Components

export default function App() {
const language = useRecoilValue(languageAtom);
const cluster = useRecoilValue(clusterState);
Expand Down
2 changes: 2 additions & 0 deletions src/components/Extensibility/ExtensibilityList.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { useFeature } from 'hooks/useFeature';
import { createPortal } from 'react-dom';
import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog';

import '../../web-components/eventListenerTracker';

export const ExtensibilityListCore = ({
resMetaData,
filterFunction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const DynamicPageComponent = ({
protectedResource,
protectedResourceWarning,
className,
customActionIfFormOpen,
}) => {
const [showTitleDescription, setShowTitleDescription] = useState(false);
const [layoutColumn, setLayoutColumn] = useRecoilState(columnLayoutState);
Expand Down Expand Up @@ -251,6 +252,16 @@ export const DynamicPageComponent = ({
headerContent={customHeaderContent ?? headerContent}
selectedSectionId={selectedSectionIdState}
onBeforeNavigate={e => {
if (customActionIfFormOpen) {
customActionIfFormOpen(
isResourceEdited,
setIsResourceEdited,
isFormOpen,
setIsFormOpen,
);
return;
}

if (isFormOpen.formOpen) {
e.preventDefault();
}
Expand Down
Loading
Loading