管理 UI

このページ

プラグインはカスタムページとダッシュボードウィジェットで管理パネルを拡張できます。これらはコアの管理機能と一緒にレンダリングされる React コンポーネントです。

管理エントリポイント

管理 UI を持つプラグインは admin エントリポイントからコンポーネントをエクスポートします。

import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";

// ダッシュボードウィジェット
export const widgets = {
	"seo-overview": SEODashboardWidget,
};

// 管理ページ
export const pages = {
	"/settings": SEOSettingsPage,
};

package.json でエントリポイントを設定します。

{
	"exports": {
		".": "./dist/index.js",
		"./admin": "./dist/admin.js"
	}
}

プラグイン定義で参照します。

definePlugin({
	id: "seo",
	version: "1.0.0",

	admin: {
		entry: "@my-org/plugin-seo/admin",
		pages: [{ path: "/settings", label: "SEO Settings", icon: "settings" }],
		widgets: [{ id: "seo-overview", title: "SEO Overview", size: "half" }],
	},
});

管理ページ

管理ページはフック経由でプラグインコンテキストを受け取る React コンポーネントです。

ページ定義

admin.pages でページを定義します。

admin: {
	pages: [
		{
			path: "/settings", // URL パス(プラグインベースからの相対)
			label: "Settings", // サイドバーラベル
			icon: "settings", // アイコン名(任意)
		},
		{
			path: "/reports",
			label: "Reports",
			icon: "chart",
		},
	];
}

ページは /_emdash/admin/plugins/<plugin-id>/<path> にマウントされます。

ページコンポーネント

import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";

export function SettingsPage() {
  const api = usePluginAPI();
  const [settings, setSettings] = useState<Record<string, unknown>>({});
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    api.get("settings").then(setSettings);
  }, []);

  const handleSave = async () => {
    setSaving(true);
    await api.post("settings/save", settings);
    setSaving(false);
  };

  return (
    <div>
      <h1>Plugin Settings</h1>

      <label>
        Site Title
        <input
          type="text"
          value={settings.siteTitle || ""}
          onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
        />
      </label>

      <label>
        <input
          type="checkbox"
          checked={settings.enabled ?? true}
          onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })}
        />
        Enabled
      </label>

      <button onClick={handleSave} disabled={saving}>
        {saving ? "Saving..." : "Save Settings"}
      </button>
    </div>
  );
}

プラグイン API フック

usePluginAPI() でプラグインのルートを呼び出します。

import { usePluginAPI } from "@emdash-cms/admin";

function MyComponent() {
	const api = usePluginAPI();

	// プラグインルートへの GET リクエスト
	const data = await api.get("status");

	// ボディ付き POST リクエスト
	await api.post("settings/save", { enabled: true });

	// URL パラメータ付き
	const result = await api.get("history?limit=50");
}

このフックはルート URL にプラグイン ID プレフィックスを自動追加します。

ダッシュボードウィジェット

ウィジェットは管理ダッシュボードに表示され、一目で情報を提供します。

ウィジェット定義

admin.widgets でウィジェットを定義します。

admin: {
	widgets: [
		{
			id: "seo-overview", // 一意のウィジェット ID
			title: "SEO Overview", // ウィジェットタイトル(任意)
			size: "half", // "full" | "half" | "third"
		},
	];
}

ウィジェットコンポーネント

import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";

export function SEOWidget() {
  const api = usePluginAPI();
  const [data, setData] = useState({ score: 0, issues: [] });

  useEffect(() => {
    api.get("analyze").then(setData);
  }, []);

  return (
    <div className="widget-content">
      <div className="score">{data.score}%</div>
      <ul>
        {data.issues.map((issue, i) => (
          <li key={i}>{issue.message}</li>
        ))}
      </ul>
    </div>
  );
}

ウィジェットサイズ

サイズ説明
fullダッシュボード全幅
halfダッシュボード半幅
thirdダッシュボード 3 分の 1 幅

ウィジェットは画面幅に応じて自動的に折り返します。

エクスポート構造

管理エントリポイントは 2 つのオブジェクトをエクスポートします。

import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
import { OverviewWidget } from "./components/OverviewWidget";

// パスをキーとしたページ
export const pages = {
	"/settings": SettingsPage,
	"/reports": ReportsPage,
};

// ID をキーとしたウィジェット
export const widgets = {
	status: StatusWidget,
	overview: OverviewWidget,
};

管理コンポーネントの使用

EmDash は一般的なパターン用にビルド済みコンポーネントを提供します。

import {
  Card,
  Button,
  Input,
  Select,
  Toggle,
  Table,
  Pagination,
  Alert,
  Loading
} from "@emdash-cms/admin";

function SettingsPage() {
  return (
    <Card title="Settings">
      <Input label="API Key" type="password" />
      <Toggle label="Enabled" defaultChecked />
      <Button variant="primary">Save</Button>
    </Card>
  );
}

自動生成される設定 UI

プラグインが設定フォームのみ必要な場合、カスタムコンポーネントなしで admin.settingsSchema を使用します。

admin: {
  settingsSchema: {
    apiKey: { type: "secret", label: "API Key" },
    enabled: { type: "boolean", label: "Enabled", default: true }
  }
}

EmDash が設定ページを自動生成します。基本設定以上の機能が必要な場合にのみカスタムページを追加してください。

ナビゲーション

プラグインページはプラグイン名の下に管理サイドバーに表示されます。順序は admin.pages 配列に一致します。

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" }, // 1 番目
		{ path: "/history", label: "History", icon: "history" }, // 2 番目
		{ path: "/reports", label: "Reports", icon: "chart" }, // 3 番目
	];
}

ビルド設定

管理コンポーネントには別のビルドエントリポイントが必要です。バンドラーを設定します。

tsdown

export default {
  entry: {
    index: "src/index.ts",
    admin: "src/admin.tsx"
  },
  format: "esm",
  dts: true,
  external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]
};

tsup

export default {
  entry: ["src/index.ts", "src/admin.tsx"],
  format: "esm",
  dts: true,
  external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]
};

React と EmDash admin は外部依存として保持し、重複バンドルを避けてください。

プラグインの有効化/無効化

管理画面でプラグインが無効化された場合:

  • サイドバーリンクは非表示
  • ダッシュボードウィジェットはレンダリングされない
  • 管理ページは 404 を返す
  • バックエンドフックは引き続き実行(データ安全のため)

プラグインは有効状態を確認できます。

const enabled = await ctx.kv.get<boolean>("_emdash:enabled");

例: 完全な管理 UI

import { definePlugin } from "emdash";

export default definePlugin({
	id: "analytics",
	version: "1.0.0",

	capabilities: ["network:fetch"],
	allowedHosts: ["api.analytics.example.com"],

	storage: {
		events: { indexes: ["type", "createdAt"] },
	},

	admin: {
		entry: "@my-org/plugin-analytics/admin",
		settingsSchema: {
			trackingId: { type: "string", label: "Tracking ID" },
			enabled: { type: "boolean", label: "Enabled", default: true },
		},
		pages: [
			{ path: "/dashboard", label: "Dashboard", icon: "chart" },
			{ path: "/settings", label: "Settings", icon: "settings" },
		],
		widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
	},

	routes: {
		stats: {
			handler: async (ctx) => {
				const today = new Date().toISOString().split("T")[0];
				const count = await ctx.storage.events!.count({
					createdAt: { gte: today },
				});
				return { today: count };
			},
		},
	},
});
import { EventsWidget } from "./components/EventsWidget";
import { DashboardPage } from "./components/DashboardPage";
import { SettingsPage } from "./components/SettingsPage";

export const widgets = {
	"events-today": EventsWidget,
};

export const pages = {
	"/dashboard": DashboardPage,
	"/settings": SettingsPage,
};