Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose Monaco and Dynamic Page as Web Components #3579

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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.
114 changes: 114 additions & 0 deletions docs/custom-extensions/busola-web-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Custom Busola Web Components

Busola provides a suite of custom Web Components to enhance your application's functionality. These components come with utility functions to dynamically update their properties, attributes, and slots after initialization.

## Utility Functions

All custom Web Components expose methods to dynamically update their properties, attributes, and slots after initialization.
String and boolean properties can be passed as standard HTML attributes. For example:

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

Functions, objects, and arrays can be passed using the `setProp` function. For example:

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

HTML elements can be passed 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.
The `Monaco Editor` web component 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 (e.g., `javascript`, `json`). Defaults to `javascript`. |
| **height** | No | string | Specifies the height of the component. Must include the unit (e.g., `100px`, `50vh`). |
| **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. |

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 is used to display content on the page and consisting of a title, header, a content area, an optional inline edit form and floating footer.
The `Dynamic Page` 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 user tries to navigate out of the Edit form tab. It recieves 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 recieves 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.
7 changes: 7 additions & 0 deletions examples/busola-web-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 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.

# Set Up Your Custom Busola Extension

To set up and deploy this exmaple, follow [this]('examples/../../custom-extension/README.md) documentation.
60 changes: 60 additions & 0 deletions examples/busola-web-components/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
document.getElementsByClassName('monaco')[0].setProp('on-change', value => {
document.getElementsByClassName('monaco')[0].setAttribute('value', value);
});

function renderEditForm(stickyHeaderHeight) {
const formContainer = document.createElement('div');
formContainer.classList.add('edit-form-test');

const form = document.createElement('form');

const nameLabel = document.createElement('label');
nameLabel.setAttribute('for', 'name');
nameLabel.textContent = 'Name:';
const nameInput = document.createElement('input');
nameInput.setAttribute('type', 'text');
nameInput.setAttribute('id', 'name');
nameInput.setAttribute('name', 'name');
nameInput.setAttribute('value', 'John Doe');
nameInput.setAttribute('placeholder', 'Enter your name');

const submitButton = document.createElement('button');
submitButton.textContent = 'Submit';
submitButton.setAttribute('type', 'submit');
submitButton.disabled = false;

form.appendChild(nameLabel);
form.appendChild(nameInput);
form.appendChild(submitButton);

formContainer.appendChild(form);

return formContainer;
}

document
.getElementsByClassName('dynamic-page')[0]
.setProp('inline-edit-form', renderEditForm);

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

document
.getElementsByClassName('dynamic-page')[0]
.setProp('custom-action-if-form-open', handleActionIfFormOpen);

const renderContent = () => {
const text = document.createElement('ui5-text');
text.innerText = 'Lorem ipsum.';
return text;
};

document
.getElementsByClassName('dynamic-page')[0]
.setSlot('content', renderContent());
16 changes: 16 additions & 0 deletions examples/busola-web-components/ui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div>
<ui5-title>Web components example</ui5-title>
<ui5-panel header-text="Dynamic Page example">
<dynamic-page-component title="Dynamic Page" class="dynamic-page">
</dynamic-page-component>
</ui5-panel>
<ui5-panel header-text="Monaco editor example">
<monaco-editor
class="monaco"
value=""
language="javascript"
height="200px"
placeholder="Write something!"
></monaco-editor>
</ui5-panel>
</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
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
69 changes: 69 additions & 0 deletions src/web-components/DynamicPageWebComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { RecoilRoot } from 'recoil';
import createWebComponent from './createWebComponent';
import { DynamicPageComponent } from 'shared/components/DynamicPageComponent/DynamicPageComponent';
import { ThemeProvider } from '@ui5/webcomponents-react';

function DynamicPageWithRecoil(props) {
const transformedForm = stickyHeaderHeight => {
if (props.inlineEditForm)
return (
<div
dangerouslySetInnerHTML={{
__html: props.inlineEditForm(stickyHeaderHeight).outerHTML,
}}
/>
);
else return null;
};

return (
<RecoilRoot>
<ThemeProvider>
<DynamicPageComponent
{...props}
inlineEditForm={props?.inlineEditForm ? transformedForm : undefined}
/>
</ThemeProvider>
</RecoilRoot>
);
}

createWebComponent(
'dynamic-page-component',
DynamicPageWithRecoil,
{
headerContent: null,
title: '',
description: '',
actions: null,
children: null,
columnWrapperClassName: '',
content: null,
footer: null,
layoutNumber: null,
layoutCloseUrl: null,
inlineEditForm: null,
showYamlTab: false,
protectedResource: false,
protectedResourceWarning: null,
className: '',
customActionIfFormOpen: undefined,
},
[
'header-content',
'title',
'description',
'actions',
'column-wrapper-class-name',
'content',
'footer',
'layout-number',
'layout-close-url',
'inline-edit-form',
'show-yaml-tab',
'protected-resource',
'protected-resource-warning',
'class-name',
'custom-action-if-form-open',
], // Observed attributes
);
42 changes: 42 additions & 0 deletions src/web-components/MonacoWebComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Editor } from 'shared/components/MonacoEditorESM/Editor';
import { RecoilRoot } from 'recoil';
import createWebComponent from './createWebComponent';

function EditorWithRecoil(props) {
return (
<RecoilRoot>
<Editor {...props} />
</RecoilRoot>
);
}

createWebComponent(
'monaco-editor',
EditorWithRecoil,
{
value: '',
language: 'javascript',
height: '300px',
readOnly: false,
placeholder: null,
onChange: undefined,
onMount: undefined,
updateValueOnParentChange: undefined,
autocompletionDisabled: false,
onBlur: undefined,
onFocus: undefined,
},
[
'value',
'language',
'height',
'read-only',
'placeholder',
'on-change',
'on-mount',
'update-value-on-parent-change',
'autocompletion-disabled',
'on-blur',
'on-focus',
],
); // Observed attributes
Loading
Loading