Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e464f3
feat(plugins): implement Plugin UI Extensions system (Phase 2)
claude Mar 15, 2026
fa77a33
docs(plugins): update documentation for Plugin UI Extensions Phase 2
claude Mar 15, 2026
53485ec
feat(plugins): add JSON Viewer example plugin for UI Extensions
claude Mar 15, 2026
065307c
feat(plugins): support UI-only plugins and external UI bundles
debba Mar 16, 2026
57b89fc
feat(plugin-modal): add plugin modal context and provider
debba Mar 18, 2026
192ce6b
Merge branch 'main' into feat/plugin-system-ui
debba Mar 18, 2026
c3bbc2b
Merge branch 'main' into feat/plugin-system-ui
debba Mar 18, 2026
6725a6c
feat(plugins): add plugin slots and external opener support
debba Mar 18, 2026
df07072
feat(plugins): add manage_tables capability and UI gating
debba Mar 20, 2026
66688f7
chore(release): bump version to 0.9.10
debba Mar 23, 2026
4373b9f
feat(modals): add error modal and use it for async errors
debba Mar 23, 2026
11ede19
test(tests): provide SettingsContext in tests and mock
debba Mar 23, 2026
ef80f2a
refactor(typescript): use explicit type guards and casts for safety
debba Mar 23, 2026
6a2a0c5
feat(plugins): default manage_tables to true and use helper
debba Mar 23, 2026
2846a2c
feat(drivers): add readonly capability to disable data writes
debba Mar 23, 2026
67b12af
docs(plugins): clarify UI extension spec and slot contexts
debba Mar 23, 2026
61a7f13
refactor(ui-and-utils): standardize component typings and plugin
debba Mar 23, 2026
8c3ae3a
chore(deps): remove tailwind-merge dependency
debba Mar 23, 2026
c87728d
Merge remote-tracking branch 'origin/main' into feat/plugin-system-ui
debba Mar 25, 2026
931b66a
Merge branch main
debba Apr 1, 2026
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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"react-dom": "^19.2.0",
"react-i18next": "^16.5.4",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.4.0",
"util": "^0.12.5",
"wkx": "^0.5.0"
},
Expand Down
232 changes: 224 additions & 8 deletions plugins/PLUGIN_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ The manifest tells Tabularis everything about your plugin.
| `connection_string_example` | string | Optional placeholder example shown in the connection string import field (e.g. `"clickhouse://user:pass@localhost:9000/db"`). Also accepted as camelCase `connectionStringExample`. |
| `identifier_quote` | string | Character used to quote SQL identifiers. Use `"\""` for ANSI standard or `` "`" `` for MySQL style. |
| `alter_primary_key` | bool | `true` if the database supports altering primary keys after table creation. |
| `manage_tables` | bool | `true` to enable table and column management UI (Create Table, Add/Modify/Drop Column, Drop Table). Does not control index or FK operations. Defaults to `true`. |
| `readonly` | bool | When `true`, the driver is read-only: all data modification operations (INSERT, UPDATE, DELETE) are disabled in the UI. The add/delete row buttons, inline cell editing, and context menu edit actions are hidden. Table and column management is also hidden regardless of `manage_tables`. Defaults to `false`. |

### Data Types

Expand Down Expand Up @@ -246,6 +248,220 @@ elif method == "initialize":

---

## 3b. UI Extensions

Plugins can inject custom React components into the host UI through a **slot-based extension system**. This is entirely optional — plugins without UI extensions continue to work as before.

### Declaring UI Extensions

Add an optional `ui_extensions` array to your `manifest.json`:

```json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"ui_extensions": [
{
"slot": "row-editor-sidebar.field.after",
"module": "ui/field-preview.js",
"order": 50
},
{
"slot": "data-grid.toolbar.actions",
"module": "ui/export-button.js"
},
{
"slot": "settings.plugin.before_settings",
"module": "ui/auth-panel.js",
"driver": "my-plugin"
}
]
}
```

#### Extension entry fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `slot` | string | yes | Target slot name (see table below). |
| `module` | string | yes | Relative path to the pre-built IIFE JavaScript bundle inside the plugin folder. |
| `order` | number | no | Sort order within the slot. Lower values render first. Default: `100`. |
| `driver` | string | no | If set, the contribution is only active when the active connection's driver matches this value. Useful for plugins that should only appear for their own driver. |

### Available Slots

| Slot Name | Location | Context Data | Use Cases |
|-----------|----------|--------------|-----------|
| `row-edit-modal.field.after` | After each field in New Row modal | `connectionId`, `tableName`, `schema`, `driver`, `columnName`, `rowData`, `isInsertion` | Validation hints, field previews |
| `row-edit-modal.footer.before` | Before Save/Cancel in New Row modal | `connectionId`, `tableName`, `schema`, `driver`, `rowData`, `isInsertion` | Batch actions, templates |
| `row-editor-sidebar.field.after` | After each field in Row Editor sidebar | `connectionId`, `tableName`, `schema`, `driver`, `columnName`, `rowData`, `rowIndex` | Field-level previews, lookups |
| `row-editor-sidebar.header.actions` | Sidebar header action buttons | `connectionId`, `tableName`, `schema`, `driver`, `rowData`, `rowIndex` | "Copy as JSON", audit links |
| `data-grid.toolbar.actions` | Table toolbar (right side) | `connectionId`, `tableName`, `schema`, `driver` | Export buttons, analysis tools |
| `data-grid.context-menu.items` | Right-click context menu on grid rows | `connectionId`, `tableName`, `schema`, `driver`, `columnName`, `rowIndex`, `rowData` | Row-level custom actions |
| `sidebar.footer.actions` | Explorer sidebar footer | `connectionId`, `driver` | Status indicators, quick actions |
| `settings.plugin.actions` | Per-plugin actions in Settings modal | `targetPluginId` | Diagnostics, re-auth buttons |
| `settings.plugin.before_settings` | Content above plugin settings form | `targetPluginId` | OAuth panels, status banners |
| `connection-modal.connection_content` | Inside the connection form | `driver` | Custom connection fields |

### SlotContext

Every slot component receives a `context` object with the fields listed above. The available fields depend on the slot — for example, `rowData` is only present for row-level slots. All fields are optional.

```typescript
interface SlotContext {
connectionId?: string | null;
tableName?: string | null;
schema?: string | null;
driver?: string | null;
rowData?: Record<string, unknown>;
columnName?: string;
rowIndex?: number;
isInsertion?: boolean;
targetPluginId?: string;
}
```

### Building UI Extension Bundles

Plugin UI components must be pre-built as **IIFE bundles** (Immediately Invoked Function Expression). The host provides `React`, `ReactJSXRuntime`, and the plugin API as globals — your bundle must **not** bundle its own copies of these.

#### Vite configuration example

```typescript
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: "src/MyComponent.tsx",
formats: ["iife"],
name: "__tabularis_plugin__",
fileName: () => "ui/my-component.js",
},
rollupOptions: {
external: ["react", "react/jsx-runtime", "@tabularis/plugin-api"],
output: {
globals: {
react: "React",
"react/jsx-runtime": "ReactJSXRuntime",
"@tabularis/plugin-api": "__TABULARIS_API__",
},
},
},
},
});
```

> **Key points:**
> - The `name` field **must** be `"__tabularis_plugin__"` — the host looks for this global.
> - The component must be the **default export** of the entry file.
> - Multiple slots can reference the same `module` file.

### Writing a Slot Component

Each component receives `context` (slot-specific data) and `pluginId` as props:

```tsx
// src/FieldPreview.tsx
import { usePluginConnection } from "@tabularis/plugin-api";

export default function FieldPreview({ context, pluginId }: {
context: Record<string, unknown>;
pluginId: string;
}) {
const { driver } = usePluginConnection();
if (context.columnName !== "geometry") return null;

return (
<div style={{ padding: "4px 0", fontSize: "11px", color: "#888" }}>
Geometry preview for {String(context.rowData?.[context.columnName])}
</div>
);
}
```

### Plugin API Hooks

Slot components can import these hooks from `@tabularis/plugin-api`:

| Hook | Returns | Purpose |
|------|---------|---------|
| `usePluginQuery()` | `(query: string) => Promise<{ columns, rows }>` | Execute read-only queries on the active connection |
| `usePluginConnection()` | `{ connectionId, driver, schema }` | Access active connection metadata |
| `usePluginToast()` | `{ showInfo(), showError(), showWarning() }` | Show toast notifications |
| `usePluginModal()` | `{ openModal(options), closeModal() }` | Open host-managed modals with custom content |
| `usePluginSetting(pluginId)` | `{ getSetting(key), setSetting(key, value) }` | Read/write plugin settings |
| `usePluginTheme()` | `{ themeId, themeName, isDark, colors }` | Access current theme info |
| `usePluginTranslation(pluginId)` | `t(key)` | Access plugin-specific i18n translations |
| `openUrl(url)` | `Promise<void>` | Open a URL in the system browser |

#### Plugin Modal

`usePluginModal()` lets you open a host-managed modal from within a slot component:

```tsx
const { openModal, closeModal } = usePluginModal();

openModal({
title: "OAuth Setup",
content: <MyOAuthForm onDone={closeModal} />,
size: "md", // "sm" | "md" | "lg" | "xl"
});
```

#### Plugin Translations

Plugins can ship locale files at `locales/{lang}.json` inside their plugin folder. The host loads them automatically and registers them under the plugin's namespace.

```
my-plugin/
├── manifest.json
├── my-plugin-binary
├── locales/
│ ├── en.json
│ └── it.json
└── ui/
└── my-component.js
```

Use `usePluginTranslation("my-plugin")` in components to access translations via `t("key")`.

### Conditional Rendering

You can control when a contribution appears using two mechanisms:

1. **`driver` field in manifest**: Set `"driver": "my-plugin"` to only render when the active connection uses that driver.
2. **Component-level filtering**: Return `null` from your component based on `context` values.

```tsx
export default function PostgresOnly({ context }: SlotComponentProps) {
// Only render for PostgreSQL connections
if (context.driver !== "postgres") return null;
return <div>PostgreSQL-specific action</div>;
}
```

### Security Restrictions

Plugin components **must not**:
- Import from `@tauri-apps/*` directly
- Access `window.__TAURI__` or invoke Tauri commands
- Manipulate the DOM outside their subtree

All host interaction goes through `@tabularis/plugin-api`.

### Error Isolation

Each contribution is wrapped in a `SlotErrorBoundary`. If your component throws, a small error badge is shown instead — other plugins and the host continue working normally.

For the full specification, see [`plugin-ui-extensions-spec.md`](../website/public/docs/plugin-ui-extensions-spec.md).

---

## 4. Implementing the JSON-RPC Interface

Your plugin must run an event loop that:
Expand Down Expand Up @@ -599,14 +815,14 @@ Update a single field in a row.
"params": ConnectionParams,
"schema": null,
"table": "users",
"primary_key_column": "id",
"primary_key_value": "42",
"column": "name",
"value": "Robert"
"pk_col": "id",
"pk_val": 42,
"col_name": "name",
"new_val": "Robert"
}
```

**Result:** `null` on success, or an error.
**Result:** Number of affected rows (e.g. `1`), or an error.

---

Expand All @@ -620,12 +836,12 @@ Delete a row from a table.
"params": ConnectionParams,
"schema": null,
"table": "users",
"primary_key_column": "id",
"primary_key_value": "42"
"pk_col": "id",
"pk_val": 42
}
```

**Result:** `null` on success, or an error.
**Result:** Number of affected rows (e.g. `1`), or an error.

---

Expand Down
13 changes: 13 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,21 @@ The full list is maintained in [`registry.json`](./registry.json).

---

## UI Extensions (Phase 2)

Starting with v0.9.15, plugins can also inject custom UI components into the Tabularis interface through a slot-based extension system. Plugins declare a `ui_extensions` array in their manifest, targeting predefined insertion points (slots) such as the toolbar, context menu, row editor, sidebar, and plugin settings page.

This is entirely optional — plugins without `ui_extensions` continue to work identically.

For details, see:
- [Plugin UI Extensions Spec](../website/public/docs/plugin-ui-extensions-spec.md) — Full specification
- [PLUGIN_GUIDE.md](./PLUGIN_GUIDE.md) § 3b — Quick-start guide for UI extensions

---

## Development Resources

- [PLUGIN_GUIDE.md](./PLUGIN_GUIDE.md) — Complete guide for implementing a plugin executable
- [Plugin UI Extensions Spec](../website/public/docs/plugin-ui-extensions-spec.md) — UI extension system specification
- [Driver Trait](../src-tauri/src/drivers/driver_trait.rs) — Rust trait all drivers implement
- [RPC Protocol](../src-tauri/src/plugins/rpc.rs) — JSON-RPC types used for communication
10 changes: 10 additions & 0 deletions plugins/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@
"type": "boolean",
"default": false,
"description": "true for API-based plugins that need no host/port/credentials (e.g. public REST APIs). Hides the entire connection form and skips database validation."
},
"manage_tables": {
"type": "boolean",
"default": true,
"description": "true if the driver supports table and column management (CREATE TABLE, ADD/MODIFY/DROP COLUMN, DROP TABLE). Does not control index or foreign key operations. Defaults to true."
},
"readonly": {
"type": "boolean",
"default": false,
"description": "When true, the driver is read-only: all data modification operations (INSERT, UPDATE, DELETE) are disabled in the UI. Table and column management is also hidden regardless of manage_tables. Defaults to false."
}
}
},
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src-tauri/capabilities/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"main"
],
"permissions": [
"updater:default"
"updater:default",
"opener:default"
]
}
Loading
Loading