Skip to content
Draft
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
291 changes: 291 additions & 0 deletions src/contents/docs/plugin-developer-guide/09.plugin-ui/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
---
title: Develop a Plugin UI
sidebarTitle: Develop a Plugin UI
icon: /src/contents/docs/icons/dev.svg
description: Learn how to build custom Vue.js UI components for your Kestra plugin, from scaffolding to Gradle integration, using Module Federation and the artifact-sdk.
---

Plugins can ship custom Vue.js frontend components that load directly into the Kestra UI at runtime, without any changes to Kestra core.

This lets you build domain-specific experiences: visualize a query plan in the topology view, render log output in a structured panel, or display task metadata in a rich card. The core UI stays lean; each plugin brings exactly the UI it needs.

## Why plugin UIs?

Tasks in Kestra produce structured outputs and have rich configuration. But the default topology and log views are generic — they show raw YAML and plain text. When a task is query-centric, graph-centric, or data-heavy, that generic view loses signal.

Plugin UIs let you close this gap without forking Kestra's core. A `topology-details` component can show a formatted query, estimated cost, or job metadata inline in the flow topology. A `log-details` component can structure log output into a readable table. This keeps the core UI generic and lets each plugin deliver the right experience for its domain.

## Architecture

Plugin UIs are built as **Vue.js micro-frontends** using [Module Federation](https://module-federation.io/). The plugin's `ui/` directory compiles to a federated JavaScript module, which is bundled into the plugin JAR under `src/main/resources/plugin-ui/`. At runtime, the Kestra host app discovers and loads these modules dynamically — no static linking required.

```
Plugin JAR
└── src/main/resources/plugin-ui/
├── plugin-ui.js ← the federated module entry point
├── manifest.json ← declares which task types have UI and which slots they fill
└── *.css ← scoped styles
```

The `manifest.json` is the contract between the plugin and the host. It tells Kestra which task types expose UI components, which slot each component fills, and any static metadata (dimensions, feature flags) the host needs before loading the component.

The [`@kestra-io/artifact-sdk`](https://github.com/kestra-io/artifact-sdk) handles all the Module Federation configuration, manifest generation, and shared dependencies. You write a Vue component; the SDK takes care of the bundling contract.

## Available UI slots

Each plugin component targets a specific **slot** — a named extension point in the Kestra UI. The SDK defines two slots:

### `topology-details`

Renders in the topology view when a task node is selected (either in the flow editor or the execution topology). Receives the task definition and, post-execution, the execution state.

```ts
interface TopologyDetailsProps {
/** The full task definition object from the flow YAML */
task: Record<string, unknown>;
/** The latest execution state for this task, if available */
execution: Record<string, unknown>;
}
```

The component is shown both before (flow design time) and after execution. Check `execution?.id` to detect which context you're in and adjust the rendered content accordingly.

### `log-details`

Renders in the log view for a task execution attempt. Receives the task definition, the list of log entries, and the attempt number.

```ts
interface LogDetailsProps {
/** The task definition */
task: Record<string, unknown>;
/** Log lines produced by this task attempt */
logs: LogEntry[];
/** The attempt number (0-indexed) */
attemptNumber: number;
}

interface LogEntry {
level: "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR";
message: string;
timestamp: string;
[key: string]: unknown;
}
```

## Quick start

Use the [`@kestra-io/create-artifact-sdk`](https://github.com/kestra-io/artifact-sdk) scaffolder to bootstrap the `ui/` directory in your plugin. Run this from your plugin's root (the directory containing `settings.gradle` or `settings.gradle.kts`):

```bash
npm create @kestra-io/artifact-sdk
```

The CLI will:

1. **Detect your plugin** — reads `settings.gradle[.kts]` to infer the plugin group ID (e.g. `io.kestra.plugin.example`).
2. **Ask which task** you want to add UI for (e.g. `query.RunQuery`).
3. **Ask which UI slot** to target (`topology-details` or `log-details`).
4. **Show a summary** and ask for confirmation before writing anything.
5. **Scaffold the `ui/` directory** with all required files and run `npm install`.

:::alert{type="info"}
Node.js ≥ 18 is required. The scaffolder can also be run from inside an existing `ui/` directory if you want to add more components later.
:::

## Project structure

After scaffolding, the `ui/` directory looks like this:

```
ui/
├── .gitignore
├── .storybook/
│ ├── main.ts
│ └── preview.ts
├── index.html ← dev server entry
├── package.json
├── tsconfig.json
├── vite.config.ts ← Module Federation config
└── src/
├── App.vue ← dev server wrapper
├── main.ts ← dev server entry
├── components/
│ └── QueryRunQueryTopologyDetails.vue ← your component to edit
└── QueryRunQueryTopologyDetails.stories.ts
```

The component file is the only file you need to edit. The rest of the scaffolding is boilerplate that wires up the local dev server, Storybook, and the production build.

## Configuring the exposed components

The `vite.config.ts` file declares which components are exposed and under which task types:

```ts
import defaultViteConfig from "@kestra-io/artifact-sdk/vite.config";

export default defaultViteConfig({
plugin: "io.kestra.plugin.example",

exposes: {
"query.RunQuery": [
{
slotName: "topology-details",
path: "./src/components/QueryRunQueryTopologyDetails.vue",
additionalProperties: {
height: 120,
heightWithExecution: 200,
},
},
],
},
});
```

- **`plugin`** — the plugin group ID, matching the prefix used in task types.
- **`exposes`** — a map from task type suffix (everything after `io.kestra.plugin.example.`) to a list of slot definitions.
- **`slotName`** — which UI slot this component fills.
- **`path`** — path to the Vue component, relative to `ui/`.
- **`additionalProperties`** — static metadata written to the manifest (see [below](#additional-properties)).

A single task can expose components for multiple slots:

```ts
"query.RunQuery": [
{
slotName: "topology-details",
path: "./src/components/QueryRunQueryTopologyDetails.vue",
additionalProperties: { height: 120 },
},
{
slotName: "log-details",
path: "./src/components/QueryRunQueryLogDetails.vue",
},
],
```

## Development workflow

### Local dev server

The scaffolded `src/App.vue` renders your component using the slot's default props. Start the dev server to iterate quickly without running Kestra:

```bash
cd ui
npm run dev
```

### Storybook

Storybook lets you develop and document your component in isolation, with full control over props:

```bash
npm run storybook
```

A starter story is generated alongside the component. Add story variants for pre-execution and post-execution states to cover both rendering modes.

### Building

```bash
npm run build -- --outDir ../src/main/resources/plugin-ui
```

The build output goes directly into the plugin's resource directory, where it will be picked up by the JAR packaging step. See [Gradle integration](#gradle-integration) to automate this.

### Testing in Kestra UI

To see your component running inside a real Kestra instance:

1. Build the UI module:

```bash
cd ui
npm run build -- --outDir ../src/main/resources/plugin-ui
```

2. Package the plugin as a JAR:

```bash
./gradlew shadowJar
```

3. Copy the JAR from `build/libs/` into your local Kestra plugins folder. Make sure there is **only one version** of the plugin JAR in that folder — remove any older versions first to avoid conflicts.

4. Restart both the Kestra backend and frontend.

5. Hard-reload the Kestra UI in your browser to bypass the cache:
- **Chrome / Firefox**: `Ctrl + Shift + R` (Linux/Windows) or `Cmd + Shift + R` (macOS)
- **Alternative**: `Ctrl + F5`

:::alert{type="info"}
The browser caches Module Federation bundles aggressively. A hard reload (`Ctrl + Shift + R`) is required after each UI build to ensure the browser fetches the latest version of your component.
:::

## Gradle integration

Add the [Node Gradle plugin](https://github.com/node-gradle/gradle-node-plugin) to your `build.gradle` and wire the UI build into the plugin packaging lifecycle:

```groovy
plugins {
// ... existing plugins ...
id 'com.github.node-gradle.node' version '7.1.0'
}

// Build the UI module before packaging (only if ui/ directory exists)
if (file('ui').exists()) {
tasks.register('npmInstallUI', com.github.gradle.node.npm.task.NpmTask) {
args = ['install']
workingDir = file('ui')
}

tasks.register('buildUI', com.github.gradle.node.npm.task.NpmTask) {
dependsOn 'npmInstallUI'
args = ['run', 'build', '--', '--outDir', '../src/main/resources/plugin-ui']
workingDir = file('ui')
}

processResources.dependsOn 'buildUI'
shadowJar.dependsOn 'buildUI'
}
```

The `if (file('ui').exists())` guard keeps the build working for other developers and CI pipelines that haven't set up Node.js, without failing the Java build.

Add the build output to `.gitignore` so the compiled assets are not committed:

```
# UI build artifact
src/main/resources/plugin-ui/
```

## Additional properties

The `additionalProperties` object in each slot definition is written verbatim into `manifest.json`. The host app reads this before loading the component, so it can reserve layout space or configure behavior without incurring the cost of loading the full module.

Commonly used properties for `topology-details`:

| Property | Type | Description |
|---|---|---|
| `height` | `number` | Height (in px) of the detail panel before execution |
| `heightWithExecution` | `number` | Height (in px) of the detail panel after execution |
| `customAction` | `object` | Adds a button on the task node that opens a drawer for a specific task property (e.g. a SQL query) |

### `customAction`

The `customAction` property lets the host render an action button directly on the task node in the topology. When clicked, the host opens a drawer displaying the specified task property with syntax highlighting:

```ts
additionalProperties: {
"customAction": {
"label": "Show query", // tooltip and button label
"taskProp": "sql", // the task property to display
"lang": "sql" // language for syntax highlighting
}
}
```

This is useful for tasks that carry large or structured payloads (SQL queries, scripts, templates) that are better viewed in a dedicated panel than inline in the YAML editor.

:::alert{type="info"}
`additionalProperties` values are static — they are evaluated at build time and embedded in the manifest. They cannot reference runtime task values.
:::
Loading