React 관리 페이지 및 위젯

이 페이지

네이티브 플러그인은 사용자 정의 React 페이지와 대시보드 위젯으로 관리 패널을 확장할 수 있습니다 — 샌드박스 플러그인은 대신 UI를 Block Kit로 설명합니다. 플러그인 JavaScript를 관리자에게 전송하면 샌드박스 격리가 깨지기 때문입니다.

플러그인이 설정 양식만 필요한 경우, 자동 생성되는 admin.settingsSchema 양식(첫 번째 네이티브 플러그인 참조)이 React를 작성하지 않고 대부분의 경우를 처리합니다. settingsSchema가 제공하는 것보다 더 풍부한 UI가 필요할 때 사용자 정의 컴포넌트를 사용하세요.

관리 진입점

관리 UI가 있는 플러그인은 admin 진입점에서 pageswidgets 객체를 내보냅니다:

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()에서 참조합니다:

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" }],
	},
});

디스크립터는 EmDash가 빌드 시 컴포넌트를 찾을 위치를 알 수 있도록 일치하는 adminEntry가 필요합니다:

adminEntry: "@my-org/plugin-seo/admin",

관리 페이지

관리 페이지는 /_emdash/admin/plugins/<plugin-id>/<path> 아래에 마운트되는 React 컴포넌트입니다.

페이지 정의

admin: {
	pages: [
		{
			path: "/settings",
			label: "Settings",
			icon: "settings",
		},
		{
			path: "/reports",
			label: "Reports",
			icon: "chart",
		},
	],
}

페이지 컴포넌트

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 as string) || ""}
					onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
				/>
			</label>

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

플러그인 API 훅

usePluginAPI()는 플러그인 ID 접두사와 자동으로 추가되는 X-EmDash-Request: 1 CSRF 헤더를 사용하여 플러그인의 라우트를 호출합니다:

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

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

	const data = await api.get("status");                       // GET /_emdash/api/plugins/<id>/status
	await api.post("settings/save", { enabled: true });          // JSON 본문이 있는 POST
	const result = await api.get("history?limit=50");            // 쿼리 매개변수 지원
}

대시보드 위젯

위젯은 관리 대시보드에 표시되며 한눈에 정보를 제공합니다.

위젯 정의

admin: {
	widgets: [
		{
			id: "seo-overview",
			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 as { message: string }).message}</li>
				))}
			</ul>
		</div>
	);
}

위젯 크기

SizeDescription
full전체 대시보드 너비
half대시보드 너비의 절반
third대시보드 너비의 1/3

위젯은 화면 너비에 따라 자동으로 줄바꿈됩니다.

내보내기 구조

관리 진입점은 두 개의 객체를 내보냅니다:

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,
};

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는 자동으로 설정 페이지를 생성합니다. 기본 양식 이상의 동작이 필요한 경우에만 사용자 정의 React 페이지를 사용하세요.

내비게이션

플러그인 페이지는 플러그인 이름 아래 관리 사이드바에 표시됩니다. 순서는 admin.pages 배열과 일치합니다.

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // 첫 번째
		{ path: "/history", label: "History", icon: "history" },     // 두 번째
		{ path: "/reports", label: "Reports", icon: "chart" },        // 세 번째
	],
}

빌드 구성

관리 컴포넌트에는 별도의 빌드 진입점이 필요합니다. 번들러를 구성합니다:

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");

완전한 예제

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

export function analyticsPlugin(): PluginDescriptor {
	return {
		id: "analytics",
		version: "1.0.0",
		format: "native",
		entrypoint: "@my-org/plugin-analytics",
		adminEntry: "@my-org/plugin-analytics/admin",
		adminPages: [
			{ path: "/dashboard", label: "Dashboard", icon: "chart" },
			{ path: "/settings", label: "Settings", icon: "settings" },
		],
		adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
	};
}

export function createPlugin() {
	return definePlugin({
		id: "analytics",
		version: "1.0.0",

		capabilities: ["network:request"],
		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 };
				},
			},
		},
	});
}

export default createPlugin;
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,
};