View .md

Power SDK reference

The window.FeatherPanel host API (version 2) for plugin frontends: 13 events, 10 cancellable actions, the payload of each, what the host reads back, and examples.

Last synced from FeatherPanel 15b94e9 on .

On this page

Code that runs in the panel window (your plugin's Frontend/index.js, see Frontend) gets window.FeatherPanel. Plugin pages in iframes reach the same bus through postMessage. Events are notifications: the panel tells you something happened. Actions are hooks that run before the panel does something, and can change or cancel it. The tables below are read from the panel source, so the payload keys are the ones the panel actually sends.

Host API

window.FeatherPanel.version is 2. The api methods call plain axios with the URL you pass (use the full path, for example /api/user/session), so same-origin requests carry the user's session cookie. They resolve to the response body, run the fp:api:request action first and emit fp:api:error when the request fails. navigate() runs fp:nav:push first. on, register, contribute and subscribe return a function that removes what they added.

interface FeatherPanelHostApi {
    version: number;
    events: {
        on: (event: string, handler: EventHandler) => () => void;
        once: (event: string, handler: EventHandler) => () => void;
        off: (event: string, handler: EventHandler) => void;
        emit: (event: string, payload?: unknown) => void;
    };
    actions: {
        register: (actionId: string, registration: ActionRegistration) => () => void;
        unregister: (actionId: string, handlerId: string) => void;
        run: (actionId: string, ctx?: Record<string, unknown>) => Promise<ActionContext>;
    };
    theme: {
        getMode: () => 'light' | 'dark';
        getAccent: () => string;
        getPackId: () => string;
        subscribe: (listener: () => void) => () => void;
    };
    api: {
        get: <T = unknown>(url: string, config?: object) => Promise<T>;
        post: <T = unknown>(url: string, data?: unknown, config?: object) => Promise<T>;
        put: <T = unknown>(url: string, data?: unknown, config?: object) => Promise<T>;
        delete: <T = unknown>(url: string, config?: object) => Promise<T>;
    };
    toast: {
        success: (message: string) => void;
        error: (message: string) => void;
        info: (message: string) => void;
        message: (message: string) => void;
    };
    navigate: (path: string) => void;
    context: {
        getPathname: () => string;
        getUser: () => unknown;
        getUiPackId: () => string;
        subscribe: (listener: () => void) => () => void;
    };
    ui: {
        modal: {
            register: (def: PluginModalDefinition) => () => void;
            open: (id: string, props?: Record<string, unknown>) => void;
            close: () => void;
        };
    };
    search: {
        contribute: (provider: PluginSearchProvider) => () => void;
        query: (q: string) => Promise<PluginSearchResult[]>;
    };
    shortcuts: {
        register: (shortcut: PluginShortcut) => () => void;
    };
}

Events

Subscribe with events.on(id, handler). The handler gets one payload object with these keys.

EventPayload keysSent from
fp:host:readyFP_EVENTS.HOST_READYversion
fp:route:changeFP_EVENTS.ROUTE_CHANGEpathname, prev
fp:page:mountFP_EVENTS.PAGE_MOUNTpathname, area
fp:page:unmountFP_EVENTS.PAGE_UNMOUNTpathname
fp:theme:changeFP_EVENTS.THEME_CHANGEtheme, accentColor, themePackId
fp:uipack:changeFP_EVENTS.UI_PACK_CHANGEuiPackId
fp:session:changeFP_EVENTS.SESSION_CHANGEuser, authenticated
fp:server:contextFP_EVENTS.SERVER_CONTEXTuuidShort, entering, pathname
fp:server:power:resultFP_EVENTS.SERVER_POWER_RESULTuuid, action, ok, cancelled, reason, error
fp:server:console:readyFP_EVENTS.SERVER_CONSOLE_READYuuid, uuidShort
fp:files:savedFP_EVENTS.FILES_SAVEDuuid, path
fp:files:selectionFP_EVENTS.FILES_SELECTION-not sent
fp:api:errorFP_EVENTS.API_ERRORmethod, url, error

Actions

Register with actions.register(id, { id, priority, handler }). "Cancellable" means the panel checks ctx.cancelled after the chain and stops. "Reads back" lists the context fields the panel uses after the chain, so changing them changes what the panel does.

ActionContext keysCancellableReads back
fp:server:powerServerConsolePage.tsx:549action, uuid, uuidShortyesaction
fp:files:savefiles-api.ts:267uuid, path, contentyespath, content
fp:files:uploadnot run by the panel---
fp:nav:pushFeatherPanelHost.tsx:127, host-api.ts:116pathyespath
fp:form:submitdata-fp-action.ts:86formId, action, method, data, sourceyessubmitNative
fp:ui:confirmServerConsolePage.tsx:567kind, uuid, messageyes-
fp:api:requesthost-api.ts:37method, url, data, configyesurl, data, config
fp:search:queryuseGlobalSearchItems.ts:685queryyesquery
fp:shortcut:invokePluginShortcutHost.tsx:50shortcutId, combo, actionIdyes-
fp:data:actiondata-fp-action.ts:56actionIdno-

fp:form:submit only runs for forms that carry data-fp-action="fp:form:submit"; a form with any other data-fp-action value runs that action instead. A cancelled fp:ui:confirm stops a server kill. fp:data:action runs after every data-fp-action click that was not cancelled, with the action id and the element's payload, for plugins that want to observe clicks.

How actions run

  • Handlers run highest priority first (default 0). Registering the same handler id again replaces the old one.
  • Each handler receives (ctx, next). Call await next() to run the rest of the chain. A handler that returns without calling next() stops the chain without cancelling the action.
  • ctx.cancel(reason) sets ctx.cancelled and ctx.cancelReason (default 'cancelled'). Later handlers do not run.
  • The context object is shared, so a handler can change fields for later handlers and for the panel.
  • actions.run(id, ctx) runs a chain yourself, so plugins can define their own actions for other plugins to hook.

Actions from HTML

The panel watches the page for clicks on elements with data-fp-action and for submits of forms with data-fp-action. It prevents the default behaviour and runs the named action:

TriggerContextAfter the chain
[data-fp-action] click...payload, elementId, href, sourceUnless cancelled, runs fp:data:action and then navigates to ctx.href (or the element's href) if there is one.
form[data-fp-action] submitformId, action, method, data, sourceUnless cancelled, submits the form natively when ctx.submitNative is true.

data-fp-payload must be JSON; anything else arrives as { raw: '...' }.

Examples

Confirm a power action

// Frontend/index.js
const fp = window.FeatherPanel;

// Ask before a server is stopped. ctx.action is 'start', 'stop', 'restart' or 'kill'.
fp?.actions.register('fp:server:power', {
  id: 'myplugin-confirm-stop',
  priority: 10,
  handler: async (ctx, next) => {
    if (ctx.action === 'stop' && !window.confirm('Stop this server?')) {
      ctx.cancel('Stop cancelled');
      return;
    }
    await next();
  },
});

// Every power request reports back, including cancelled ones.
fp?.events.on('fp:server:power:result', (p) => {
  if (p.cancelled) console.info('[myplugin] power action cancelled:', p.reason);
});

Change a file before it is saved

// Normalise line endings before a file is written. The host reads ctx.path and ctx.content back.
window.FeatherPanel?.actions.register('fp:files:save', {
  id: 'myplugin-crlf',
  handler: async (ctx, next) => {
    if (typeof ctx.content === 'string' && ctx.path.endsWith('.properties')) {
      ctx.content = ctx.content.replace(/\r\n/g, '\n');
    }
    await next();
  },
});

Search, shortcuts and modals

const fp = window.FeatherPanel;

// Results in the panel's global search
fp?.search.contribute({
  id: 'myplugin-search',
  search: async (query) => [
    { id: 'myplugin-docs', title: 'My plugin settings', description: query, href: '/admin/myplugin', category: 'My plugin' },
  ],
});

// A keyboard shortcut that runs a direct handler
fp?.shortcuts.register({
  id: 'myplugin-open',
  combo: 'ctrl+shift+m',
  description: 'Open My plugin',
  handler: () => fp.navigate('/admin/myplugin'),
});

// A modal that shows a page from your plugin's public folder
fp?.ui.modal.register({ id: 'myplugin-modal', title: 'My plugin', componentUrl: '/components/myplugin/modal.html', size: 'md' });
fp?.ui.modal.open('myplugin-modal');

Buttons and forms

<!-- Runs the action "myplugin:refresh" with { scope: 'all', elementId, href, source: 'data-fp-action' } -->
<button id="refresh" data-fp-action="myplugin:refresh" data-fp-payload='{"scope":"all"}'>Refresh</button>

<!-- Runs "myplugin:subscribe" with { formId, action, method, data, source: 'data-fp-form' } -->
<form id="subscribe" data-fp-action="myplugin:subscribe" method="post">
  <input name="email" type="email" />
  <button type="submit">Subscribe</button>
</form>

<script>
  window.FeatherPanel?.actions.register('myplugin:subscribe', {
    id: 'myplugin-subscribe',
    handler: async (ctx, next) => {
      await window.FeatherPanel.api.post('/api/myplugin/subscribe', ctx.data);
      ctx.submitNative = false; // true submits the form normally after the chain
      await next();
    },
  });
</script>

Types

type ActionContext = Record<string, unknown> & {
    /** Set by cancel() - when true, the host must abort the action. */
    cancelled?: boolean;
    cancelReason?: string;
    cancel: (reason?: string) => void;
};

type ActionHandler = (ctx: ActionContext, next: () => Promise<void>) => void | Promise<void>;

type ActionRegistration = {
    /** Unique handler id (plugin-scoped). */
    id: string;
    /** Higher runs earlier. Default 0. */
    priority?: number;
    handler: ActionHandler;
};

type PluginModalDefinition = {
    id: string;
    title?: string;
    /** Public URL to HTML iframe */
    componentUrl?: string;
    /** Public URL to ESM React remote */
    remoteUrl?: string;
    /** Optional width class hint */
    size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
};

type PluginSearchResult = {
    id: string;
    title: string;
    description?: string;
    href?: string;
    category?: string;
    onSelect?: () => void;
};

type PluginSearchProvider = {
    id: string;
    /** Sync or async results for the current query */
    search: (query: string) => PluginSearchResult[] | Promise<PluginSearchResult[]>;
};

type PluginShortcut = {
    id: string;
    /** e.g. "ctrl+shift+m" (lowercase, + separated) */
    combo: string;
    description?: string;
    /** Run a named action hook, or a direct handler */
    actionId?: string;
    handler?: () => void | Promise<void>;
};