feat: replace core homepage widget registry with generic extension points

This commit is contained in:
LukeGus committed 2026-09-29 13:23:51 -05:00
1 parent b8bbcd2196
commit 50d43e29fb
18 files changed
+242 -147

No files matched your search

+1 -2
View File
@@ -57,6 +57,5 @@ export const dockerWidget: WidgetDefinition<DockerWidgetConfig> = {
defaultConfig: { hostId: 0 },
defaultSize: { w: GRID_SIZE * 20, h: GRID_SIZE * 14 },
minSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 8 },
component: DockerWidget,
editFormComponent: DockerWidgetEditForm,
components: { view: DockerWidget, editForm: DockerWidgetEditForm },
};
+17 -5
View File
@@ -1,4 +1,5 @@
import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend";
import type { ComponentType, ReactNode } from "react";
import type { ExtensionContribution } from "@termix/plugin-sdk/frontend";
/** The homepage's grid step, in pixels. */
export const GRID_SIZE = 30;
@@ -18,7 +19,18 @@ export interface WidgetEditFormProps<C> {
onChange: (config: C) => void;
}
export type WidgetDefinition<C> = Omit<
HomepageWidgetContribution<C>,
"defaultConfig"
> & { defaultConfig: C };
/** What app.registerExtension("homepage.widgets", ...) takes. */
export interface WidgetDefinition<C> extends ExtensionContribution {
id: string;
name: string;
description: string;
category: "links" | "info" | "system" | "monitoring";
icon: ReactNode;
defaultConfig: C;
defaultSize: { w: number; h: number };
minSize: { w: number; h: number };
components: {
view: ComponentType<WidgetComponentProps<C>>;
editForm?: ComponentType<WidgetEditFormProps<C>>;
};
}
+1 -4
View File
@@ -1,7 +1,6 @@
import type { ComponentType } from "react";
import { Box } from "lucide-react";
import type {
HomepageWidgetContribution,
StandaloneViewProps,
TabProps,
TermixApp,
@@ -57,9 +56,7 @@ export function activate(app: TermixApp): void {
when: (host) => canUse && !!host.enableSsh && dockerEnabled(host),
});
app.registerHomepageWidget(
dockerWidget as unknown as HomepageWidgetContribution,
);
app.registerExtension("homepage.widgets", dockerWidget);
app.registerSlotContribution("onboarding.features", {
actionId: "docker.feature",
@@ -1,4 +1,5 @@
import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend";
import type { ComponentType, ReactNode } from "react";
import type { ExtensionContribution } from "@termix/plugin-sdk/frontend";
/** The homepage's grid step, in pixels. */
export const GRID_SIZE = 30;
@@ -18,7 +19,18 @@ export interface WidgetEditFormProps<C> {
onChange: (config: C) => void;
}
export type WidgetDefinition<C> = Omit<
HomepageWidgetContribution<C>,
"defaultConfig"
> & { defaultConfig: C };
/** What app.registerExtension("homepage.widgets", ...) takes. */
export interface WidgetDefinition<C> extends ExtensionContribution {
id: string;
name: string;
description: string;
category: "links" | "info" | "system" | "monitoring";
icon: ReactNode;
defaultConfig: C;
defaultSize: { w: number; h: number };
minSize: { w: number; h: number };
components: {
view: ComponentType<WidgetComponentProps<C>>;
editForm?: ComponentType<WidgetEditFormProps<C>>;
};
}
+11 -5
View File
@@ -8,7 +8,11 @@ import {
type TabProps,
type TermixApp,
} from "@termix/plugin-sdk/frontend";
import { GRID_SIZE } from "./homepage/homepage.js";
import {
GRID_SIZE,
type FileManagerWidgetConfig,
type WidgetDefinition,
} from "./homepage/homepage.js";
import { FileManager } from "./FileManager.tsx";
import FileManagerApp from "./FileManagerApp.tsx";
import { SftpTransferTab } from "./SftpTransferTab.tsx";
@@ -113,7 +117,7 @@ export function activate(app: TermixApp): void {
fileManagerHostSetting(host, "enableFileManager", true),
});
app.registerHomepageWidget({
app.registerExtension("homepage.widgets", {
id: "file_manager_widget",
name: "File Manager",
description: "Embedded SFTP file manager for a configured host",
@@ -122,9 +126,11 @@ export function activate(app: TermixApp): void {
defaultConfig: { hostId: 0 },
defaultSize: { w: GRID_SIZE * 20, h: GRID_SIZE * 14 },
minSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 8 },
component: FileManagerWidget as never,
editFormComponent: FileManagerWidgetEditForm as never,
});
components: {
view: FileManagerWidget,
editForm: FileManagerWidgetEditForm,
},
} satisfies WidgetDefinition<FileManagerWidgetConfig>);
const openHost = ((host: PluginHostRecord | null, path?: string) =>
openHostAction(app, host, path)) as never;
+6 -2
View File
@@ -10,7 +10,11 @@ import { HomepagePreviewCard } from "./HomepagePreviewCard.js";
import { ServiceLinksCard } from "./ServiceLinksCard.js";
import { DashboardHomepageView } from "./DashboardHomepageView.js";
import { setHomepageApi } from "./api.js";
import { drainQueuedWidgets } from "./widgets/WidgetRegistry.js";
import {
WIDGET_POINT,
drainQueuedWidgets,
toExtension,
} from "./widgets/WidgetRegistry.js";
// Side-effect imports so widgets register themselves.
import "./widgets/ServiceLinkWidget";
@@ -54,7 +58,7 @@ export function activate(app: TermixApp): void {
app.onDispose(() => setHomepageApi(null));
for (const widget of drainQueuedWidgets()) {
app.registerHomepageWidget(widget as never);
app.registerExtension(WIDGET_POINT, toExtension(widget));
}
app.registerTab("homepage", HomepageTab as ComponentType<TabProps>, {
@@ -1,16 +1,22 @@
import {
homepageWidgetType,
useHomepageWidgetTypes,
type HomepageWidgetContribution,
getExtension,
useExtensions,
type ExtensionContribution,
} from "@termix/plugin-sdk/frontend";
import { useMemo } from "react";
import type { WidgetTypeDefinition, WidgetTypeId } from "../types.js";
/**
* Other plugins add widgets with app.registerExtension(WIDGET_POINT, ...),
* putting the widget under components.view and its edit form under
* components.editForm. Core only stores the list, so a plugin's widgets
* stay registered while this plugin is off.
*/
export const WIDGET_POINT = "homepage.widgets";
/**
* Each widget module calls this at import time (before activate(app) runs),
* so the definitions are queued here and app.registerHomepageWidget is
* called for each one from activate. Core owns the actual registry, the same
* one other plugins (docker, tunnels, host-metrics, file-manager) register
* their own widgets into.
* so the definitions are queued here and registered from activate.
*/
const queued: WidgetTypeDefinition[] = [];
@@ -23,18 +29,42 @@ export function drainQueuedWidgets(): WidgetTypeDefinition[] {
return queued.splice(0, queued.length);
}
export function toExtension(def: WidgetTypeDefinition): ExtensionContribution {
const { component, editFormComponent, ...rest } = def;
return {
...rest,
components: editFormComponent
? { view: component, editForm: editFormComponent }
: { view: component },
};
}
function toDefinition(
widget: HomepageWidgetContribution | undefined,
extension: ExtensionContribution | undefined,
): WidgetTypeDefinition | undefined {
return widget as unknown as WidgetTypeDefinition | undefined;
const view = extension?.components?.view;
if (!extension || !view) return undefined;
const { components, ...rest } = extension;
return {
...(rest as unknown as WidgetTypeDefinition),
component: view,
editFormComponent: components?.editForm,
};
}
export function getWidgetType(
id: WidgetTypeId,
): WidgetTypeDefinition | undefined {
return toDefinition(homepageWidgetType(id));
return toDefinition(getExtension(WIDGET_POINT, id));
}
export function useWidgetTypes(): WidgetTypeDefinition[] {
return useHomepageWidgetTypes() as unknown as WidgetTypeDefinition[];
const extensions = useExtensions(WIDGET_POINT);
return useMemo(
() =>
extensions
.map(toDefinition)
.filter((def): def is WidgetTypeDefinition => !!def),
[extensions],
);
}
@@ -236,8 +236,7 @@ export const metricsChartWidget: WidgetDefinition<MetricsChartConfig> = {
},
defaultSize: { w: GRID_SIZE * 12, h: GRID_SIZE * 6 },
minSize: { w: GRID_SIZE * 6, h: GRID_SIZE * 4 },
component: MetricsChartWidget,
editFormComponent: MetricsChartEditForm,
components: { view: MetricsChartWidget, editForm: MetricsChartEditForm },
};
export { MetricsChartWidget };
+1 -4
View File
@@ -1,7 +1,6 @@
import type { ComponentType } from "react";
import { Activity, Server } from "lucide-react";
import type {
HomepageWidgetContribution,
HostEditorSectionProps,
PluginHostRecord,
StandaloneViewProps,
@@ -100,9 +99,7 @@ export async function activate(app: TermixApp): Promise<void> {
component: MetricsHostSection,
});
app.registerHomepageWidget(
metricsChartWidget as unknown as HomepageWidgetContribution,
);
app.registerExtension("homepage.widgets", metricsChartWidget);
// Live CPU, memory and disk bars in the terminal toolbar's expanded view.
app.registerSlotContribution("terminal.toolbarStatus", {
+17 -5
View File
@@ -1,4 +1,5 @@
import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend";
import type { ComponentType, ReactNode } from "react";
import type { ExtensionContribution } from "@termix/plugin-sdk/frontend";
/** The homepage's grid step, in pixels. */
export const GRID_SIZE = 30;
@@ -25,7 +26,18 @@ export interface WidgetEditFormProps<C> {
onChange: (config: C) => void;
}
export type WidgetDefinition<C> = Omit<
HomepageWidgetContribution<C>,
"defaultConfig"
> & { defaultConfig: C };
/** What app.registerExtension("homepage.widgets", ...) takes. */
export interface WidgetDefinition<C> extends ExtensionContribution {
id: string;
name: string;
description: string;
category: "links" | "info" | "system" | "monitoring";
icon: ReactNode;
defaultConfig: C;
defaultSize: { w: number; h: number };
minSize: { w: number; h: number };
components: {
view: ComponentType<WidgetComponentProps<C>>;
editForm?: ComponentType<WidgetEditFormProps<C>>;
};
}
+4 -9
View File
@@ -1,10 +1,6 @@
import type { ComponentType } from "react";
import { Network, Plug } from "lucide-react";
import type {
HomepageWidgetContribution,
TabProps,
TermixApp,
} from "@termix/plugin-sdk/frontend";
import type { TabProps, TermixApp } from "@termix/plugin-sdk/frontend";
import { TunnelTab } from "./TunnelTab";
import { HostTunnelsSection } from "./HostTunnelsSection";
import {
@@ -84,7 +80,7 @@ export function activate(app: TermixApp): void {
icon: Plug as ComponentType<{ className?: string }>,
});
app.registerHomepageWidget({
app.registerExtension("homepage.widgets", {
id: "tunnel_widget",
name: "Tunnel Manager",
description: "Embedded SSH tunnel manager for a configured host",
@@ -93,9 +89,8 @@ export function activate(app: TermixApp): void {
defaultConfig: { hostId: 0 },
defaultSize: { w: GRID_SIZE * 16, h: GRID_SIZE * 10 },
minSize: { w: GRID_SIZE * 8, h: GRID_SIZE * 6 },
component: TunnelWidget,
editFormComponent: TunnelWidgetEditForm,
} as HomepageWidgetContribution);
components: { view: TunnelWidget, editForm: TunnelWidgetEditForm },
});
// The dashboard's active tunnel counter.
app.registerAction(