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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ build/
### Mac OS ###
.DS_Store

## Directories created by Namazu Elemetns
## Directories created by Namazu Elements
/cdn-repos/**
/script-repos/**

## Frontend build tooling
/ui/node_modules/
/ui/.node/

2 changes: 2 additions & 0 deletions .idea/encodings.xml

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

12 changes: 12 additions & 0 deletions .idea/runConfigurations/build_ui.xml

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

16 changes: 16 additions & 0 deletions .idea/runConfigurations/run_element.xml

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

12 changes: 12 additions & 0 deletions .idea/runConfigurations/run_superuser_ui.xml

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

12 changes: 12 additions & 0 deletions .idea/runConfigurations/run_user_ui.xml

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

109 changes: 108 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# CLAUDE.md — element-example

This is a reference/example project for **Namazu Elements 3.7**, demonstrating how to build a custom Element using the multi-module Maven structure, REST endpoints, Guice DI, and the `.elm` archive format.
This is a reference/example project for **Namazu Elements 3.8**, demonstrating how to build a custom Element using the multi-module Maven structure, REST endpoints, Guice DI, and the `.elm` archive format.

## Project Structure

```
element-example/
├── api/ # Exported interfaces (other Elements depend on this)
├── ui/ # TypeScript/React UI plugin source (Vite; not deployed directly)
├── element/ # Implementation module — builds the .elm archive
├── debug/ # Local development runner (not deployed)
└── services-dev/ # Docker services (MongoDB) for local dev
```

**Module roles:**
- `api` — Pure interfaces + DTOs exported to other Elements via a classified JAR
- `ui` — Vite/TypeScript source for dashboard UI plugins; builds IIFE bundles into `element/src/main/ui/`
- `element` — REST endpoints, services, Guice modules; compiles to `.elm` archive
- `debug` — Local Elements runtime harness; never deployed

Expand Down Expand Up @@ -107,6 +109,111 @@ For database access, see **[MORPHIA.md](MORPHIA.md)**. It covers `Transaction`,
| `User.Level.UNPRIVILEGED` | Sentinel for unauthenticated/guest users |
| `AuthSchemes.SESSION_SECRET` | Header name for session auth (`"session_secret"`) |

## Dashboard UI Plugins

Elements can inject custom pages into the Elements dashboard by shipping a React component bundle alongside the Java code. The dashboard discovers these bundles at runtime via a `plugin.json` manifest — no dashboard changes required.

### How it works

The manifest and bundle are placed in the Element's UI content directory under a named segment:

```
element/src/main/ui/
superuser/
plugin.json # declares sidebar entry and bundle location
plugin.bundle.js # self-contained IIFE bundle (built from ui/ module)
user/
plugin.json
plugin.bundle.js
```

These are packaged into the `.elm` artifact at build time and served under `/app/ui/{element-prefix}/{segment}/`.

### plugin.json

```json
{
"schema": "1",
"entries": [
{
"label": "Example Element",
"icon": "Package",
"bundlePath": "plugin.bundle.js",
"route": "example-element"
}
]
}
```

| Field | Description |
|---|---|
| `label` | Text shown in the dashboard sidebar |
| `icon` | A [Lucide](https://lucide.dev/icons/) icon name (e.g. `Package`, `Layers`, `Zap`) |
| `bundlePath` | Path to the bundle, relative to the manifest |
| `route` | Unique key used in the dashboard URL (`/plugin/{route}`) |

### Bundle format

The bundle must be an IIFE that registers a React component with the dashboard's plugin registry. The host dashboard exposes `window.React` — the bundle must use this same instance.

```js
(function () {
var React = window.React;
function MyPlugin() {
return React.createElement('div', { className: 'p-6' }, 'My Plugin');
}
window.__elementsPlugins && window.__elementsPlugins.register('my-route', MyPlugin);
})();
```

Tailwind utility classes work out of the box — the dashboard stylesheet is already loaded.

### Developing the UI (`ui/` module)

The `ui/` Maven module is a Vite/TypeScript project that builds the IIFE bundles and writes them directly into `element/src/main/ui/{segment}/`.

**One-time setup:**
```bash
cd ui
npm install
```

**Standalone dev server (fast iteration):**
```bash
npm run dev:superuser # or: npm run dev:user
# Open http://localhost:5173
```

Edit `src/superuser/ExamplePlugin.tsx` and the browser updates instantly.

**Build for integration:**
```bash
npm run build
```

Writes `plugin.bundle.js` into `element/src/main/ui/superuser/` and `element/src/main/ui/user/`. Then restart the debug server to pick up the new bundle.

**Source layout:**
```
ui/src/
superuser/
ExamplePlugin.tsx # edit this — the component shown in the dashboard
plugin-entry.ts # registers the component with window.__elementsPlugins
dev-entry.tsx # mounts component for standalone dev (not shipped)
index.html # dev server entry point (not shipped)
user/ # same structure
shared/ # optional: components shared between segments
```

**CI / Maven build** — activate the `build-ui` profile to run npm via Maven (uses `frontend-maven-plugin`; off by default):
```bash
mvn install -Pbuild-ui
```

### User segmentation

`superuser/` serves components shown to administrators. `user/` serves components in user-facing dashboards (future). Each segment is discovered independently.

## Static & UI Content

- **`element/src/main/static/`** — static files served at `/app/static/{prefix}/`
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ Try out GET http://localhost:8080/app/rest/example-element/helloworld
> [!Note]
> The `example-element` portion of the URL is determined by the `dev.getelements.elements.app.serve.prefix` value in the `dev.getelements.element.attributes.properties` file in the deployment project.

---

## Dashboard UI Plugins

Elements can inject custom pages into the Elements dashboard by shipping a React component bundle alongside the Element's Java code. See **[ui/README.md](ui/README.md)** for the full guide.

---

# Further Reading

- **[MORPHIA.md](MORPHIA.md)** — How to use the MongoDB/Morphia database layer inside a custom Element (Transactions, DAOs, `Datastore`, `@ElementTypeRequest`)
Expand Down
25 changes: 24 additions & 1 deletion debug/src/main/java/run.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
import dev.getelements.elements.sdk.local.ElementsLocalBuilder;

import java.io.File;
import java.io.IOException;

/**
* Runs your local Element in the SDK.
*
* Working directory must be the project root (element-example/).
* IntelliJ: Run → Edit Configurations → Working directory → set to this project root.
*/
public class run {
public static void main(final String[] args ) throws IOException, InterruptedException {

// Install npm dependencies on first run, then build both segment bundles.
// The bundles are written directly to element/src/main/ui/{superuser,user}/
// so that the Maven build triggered by local.start() picks them up.
final var uiDir = new File("ui");

if (!new File(uiDir, "node_modules").exists()) {
new ProcessBuilder("npm", "install")
.directory(uiDir)
.inheritIO()
.start()
.waitFor();
}

new ProcessBuilder("npm", "run", "build")
.directory(uiDir)
.inheritIO()
.start()
.waitFor();

new ProcessBuilder("docker", "compose", "up", "-d")
.directory(new java.io.File("services-dev"))
.directory(new File("services-dev"))
.inheritIO()
.start()
.waitFor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ public class HelloWorldApplication extends Application {
@ElementDefaultAttribute("true")
public static final String AUTH_ENABLED = "dev.getelements.elements.auth.enabled";

@ElementDefaultAttribute("/element/example/api")
@ElementDefaultAttribute("/element/example/rest/api")
public static final String RS_ROOT = "dev.getelements.elements.element.rs.root";

@ElementDefaultAttribute("/element/example/ws")
public static final String WS_ROOT = "dev.getelements.elements.element.ws.root";

@ElementDefaultAttribute("/app/static/test/path")
public static final String STATIC_CONTENT_URI = "dev.getelements.element.static.uri";

@ElementDefaultAttribute("/app/ui/test/path")
public static final String UI_CONTENT_URI = "dev.getelements.element.ui.uri";

public static final String OPENAPI_TAG = "Example";

/**
Expand Down
11 changes: 11 additions & 0 deletions element/src/main/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Element Static Content</title>
</head>
<body>
<h1>Static Content</h1>
<p>This file is served from the standard static content tree of the sdk-test-element-rs test Element.</p>
</body>
</html>
11 changes: 11 additions & 0 deletions element/src/main/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Element UI Content</title>
</head>
<body>
<h1>UI Content</h1>
<p>This file is served from the UI content tree of the sdk-test-element-rs test Element. Blah blah blah.</p>
</body>
</html>
59 changes: 59 additions & 0 deletions element/src/main/ui/superuser/plugin.bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
(function(React) {
"use strict";
var _a;

function ExamplePlugin() {
var _b = React.useState(null), info = _b[0], setInfo = _b[1];
var _c = React.useState(false), loading = _c[0], setLoading = _c[1];
var _d = React.useState(null), error = _d[0], setError = _d[1];

async function fetchVersion() {
setLoading(true);
setError(null);
try {
var res = await fetch("/api/rest/version");
if (!res.ok) throw new Error(res.status + " " + res.statusText);
setInfo(await res.json());
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
}

return /* @__PURE__ */ React.createElement("div", { className: "p-6 max-w-2xl" },
/* @__PURE__ */ React.createElement("h1", { className: "text-2xl font-bold mb-2" }, "Example Element"),
/* @__PURE__ */ React.createElement("p", { className: "text-muted-foreground mb-6" },
"This page is served from the Example Element\u2019s superuser UI content directory."
),
/* @__PURE__ */ React.createElement("div", { className: "space-y-4" },
/* @__PURE__ */ React.createElement("div", null,
/* @__PURE__ */ React.createElement("button", {
onClick: fetchVersion,
disabled: loading,
className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-opacity"
}, loading ? "Loading\u2026" : "Get Platform Version")
),
error && /* @__PURE__ */ React.createElement("div", {
className: "rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive"
}, error),
info && /* @__PURE__ */ React.createElement("div", { className: "rounded-lg border p-4 text-sm space-y-1" },
/* @__PURE__ */ React.createElement("div", { className: "flex gap-2" },
/* @__PURE__ */ React.createElement("span", { className: "text-muted-foreground w-20" }, "Version"),
/* @__PURE__ */ React.createElement("span", { className: "font-mono" }, info.version)
),
/* @__PURE__ */ React.createElement("div", { className: "flex gap-2" },
/* @__PURE__ */ React.createElement("span", { className: "text-muted-foreground w-20" }, "Revision"),
/* @__PURE__ */ React.createElement("span", { className: "font-mono" }, info.revision)
),
/* @__PURE__ */ React.createElement("div", { className: "flex gap-2" },
/* @__PURE__ */ React.createElement("span", { className: "text-muted-foreground w-20" }, "Built"),
/* @__PURE__ */ React.createElement("span", { className: "font-mono" }, info.timestamp)
)
)
)
);
}

(_a = window.__elementsPlugins) == null ? void 0 : _a.register("example-element", ExamplePlugin);
})(window.React);
11 changes: 11 additions & 0 deletions element/src/main/ui/superuser/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"schema": "1",
"entries": [
{
"label": "Example Element",
"icon": "Package",
"bundlePath": "plugin.bundle.js",
"route": "example-element"
}
]
}
8 changes: 8 additions & 0 deletions element/src/main/ui/user/plugin.bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
(function(React) {
"use strict";
var _a;
function ExamplePlugin() {
return /* @__PURE__ */ React.createElement("div", { className: "p-6 max-w-2xl" }, /* @__PURE__ */ React.createElement("h1", { className: "text-2xl font-bold mb-2" }, "Example Element"), /* @__PURE__ */ React.createElement("p", { className: "text-muted-foreground mb-4" }, "This page is served from the Example Element’s user UI content directory."), /* @__PURE__ */ React.createElement("div", { className: "rounded-lg border p-4 text-sm text-muted-foreground" }, "Installed Elements can inject custom user-facing UI by placing a plugin.json and plugin.bundle.js in their ui/user/ content directory."));
}
(_a = window.__elementsPlugins) == null ? void 0 : _a.register("example-element", ExamplePlugin);
})(window.React);
Loading