Skip to content

Custom UI

When a plugin does not provide a custom UI, the Fundament console automatically generates read-only list and detail views for each CRD the plugin manages. These default views are derived directly from the CRD schema: the list view uses additionalPrinterColumns to build its table columns, and the detail view renders the resource’s spec and status fields. No extra configuration is needed to get this default UI.

Custom UIs replace the default views for the CRDs you specify. Plugin UIs run inside sandboxed iframes in the Fundament console. Each plugin serves its own HTML pages from its Go backend and includes two SDK files provided by Fundament.

Every plugin HTML page must include both SDK files. They are served by the Fundament console and are available at a stable, versioned path:

<link rel="stylesheet" href="/plugin-ui/plugin-sdk.css" />
<script src="/plugin-ui/plugin-sdk.js"></script>
FilePurpose
plugin-sdk.cssBase styles, dark-mode support, and component classes
plugin-sdk.jsHandles the host↔plugin message protocol, theme application, and iframe auto-resize

The host sends messages to the plugin after it signals readiness.

Plugin → host (sent automatically by plugin-sdk.js)

Section titled “Plugin → host (sent automatically by plugin-sdk.js)”
MessageWhenPayload
plugin:readyOn script load(no payload)
plugin:resizeWhen content height changes{ height: number }
plugin:navigateWhen the plugin navigates{ path: string }
MessageWhenPayload
fundament:initAfter plugin:ready{ theme, pluginName, crdKind, view }
fundament:theme-changedUser switches theme{ theme: 'light' | 'dark' }

The fundament:init message contains everything the plugin needs to render the correct view:

FieldDescription
theme'light' or 'dark' — applied automatically by the SDK
pluginNameName of the installed plugin
crdKindThe CRD kind being shown (e.g. certificates.cert-manager.io)
view'list' or 'detail'

plugin-sdk.css provides component classes built on Tailwind CSS. Dark mode is applied automatically by plugin-sdk.js via a .dark class on <body>.

ClassDescription
.plugin-cardBordered card container with rounded corners and padding
.plugin-headingPrimary heading (h1)
.plugin-textBody / paragraph text
.plugin-tableFull-width data table with row dividers

The plugin page is served from the plugin’s own Go backend (via ConsoleProvider). Use fetch with relative URLs to call your backend’s API:

const response = await fetch('/api/resources');
const data = await response.json();

Your Go backend handles authentication — the Fundament console proxies requests to your plugin’s service, so session credentials are not exposed to the iframe.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My plugin</title>
<link rel="stylesheet" href="/plugin-ui/plugin-sdk.css" />
</head>
<body>
<div class="plugin-card">
<h1 class="plugin-heading" id="heading">Resources</h1>
<table class="plugin-table">
<thead>
<tr>
<th>Name</th>
<th>Namespace</th>
<th>Status</th>
</tr>
</thead>
<tbody id="items">
<tr><td colspan="3" class="plugin-text">Loading…</td></tr>
</tbody>
</table>
</div>
<script src="/plugin-ui/plugin-sdk.js"></script>
<script>
window.addEventListener('message', async (event) => {
const data = event.data;
if (data?.type !== 'fundament:init') return;
// Update the heading to show the resource kind from context.
document.getElementById('heading').textContent = data.crdKind;
// Fetch resources from the plugin backend.
const response = await fetch('/api/resources');
const items = await response.json();
const tbody = document.getElementById('items');
tbody.innerHTML = '';
for (const item of items) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.metadata.name}</td>
<td>${item.metadata.namespace}</td>
<td>${item.status?.phase ?? ''}</td>
`;
tbody.appendChild(tr);
}
});
</script>
</body>
</html>

Implement ConsoleProvider in your plugin to serve the HTML pages:

//go:embed console
var consoleFS embed.FS
type MyPlugin struct { ... }
func (p *MyPlugin) ConsoleAssets() http.FileSystem {
return console.MustNewFileSystem(consoleFS)
}

The runtime mounts these assets at /console/. Your definition.yaml references them:

menu:
project:
- crd: myresources.my-api.io
list: true # served from /console/list.html
detail: true # served from /console/detail.html

See Writing a plugin for the full plugin setup.