mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: replace core homepage widget registry with generic extension points
This commit is contained in:
1 parent
b8bbcd2196
commit
50d43e29fb
18 files changed
+242
-147
No files matched your search
@@ -608,21 +608,18 @@ export interface DashboardCardContribution {
|
||||
component: ComponentType<DashboardCardProps>;
|
||||
}
|
||||
|
||||
export interface HomepageWidgetContribution<C = Record<string, unknown>> {
|
||||
/**
|
||||
* One item in a named list another plugin reads (app.registerExtension). The
|
||||
* reading plugin owns the point id and the item shape. Components go under
|
||||
* `components` so core can scope them to the registering plugin.
|
||||
*/
|
||||
export interface ExtensionContribution {
|
||||
id: string;
|
||||
/** Set by core to the registering plugin's id; a plugin never sets this itself. */
|
||||
pluginId?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: "links" | "info" | "system" | "monitoring";
|
||||
icon: ReactNode;
|
||||
defaultConfig: C;
|
||||
defaultSize: { w: number; h: number };
|
||||
minSize: { w: number; h: number };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
component: ComponentType<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editFormComponent?: ComponentType<any>;
|
||||
components?: Record<string, ComponentType<any>>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SettingsComponentProps {
|
||||
@@ -818,7 +815,10 @@ export interface TermixApp extends TermixAppInfo {
|
||||
binding: KeybindingDefaultContribution,
|
||||
) => Disposer;
|
||||
registerDashboardCard: (card: DashboardCardContribution) => Disposer;
|
||||
registerHomepageWidget: (widget: HomepageWidgetContribution) => Disposer;
|
||||
registerExtension: (
|
||||
pointId: string,
|
||||
extension: ExtensionContribution,
|
||||
) => Disposer;
|
||||
registerSettingsComponent: (
|
||||
componentId: string,
|
||||
component: ComponentType<SettingsComponentProps>,
|
||||
@@ -986,8 +986,11 @@ export interface PluginHostBridge {
|
||||
useHostActions: () => HostActionContribution[];
|
||||
useActivityTypes: () => string[];
|
||||
activityTarget: (type: string) => ActivityTargetInfo | undefined;
|
||||
useHomepageWidgetTypes: () => HomepageWidgetContribution[];
|
||||
homepageWidgetType: (id: string) => HomepageWidgetContribution | undefined;
|
||||
useExtensions: (pointId: string) => ExtensionContribution[];
|
||||
getExtension: (
|
||||
pointId: string,
|
||||
id: string,
|
||||
) => ExtensionContribution | undefined;
|
||||
core: PluginCoreApi;
|
||||
usePluginUiPreferences: (pluginId: string) => {
|
||||
values: Record<string, unknown>;
|
||||
@@ -1333,19 +1336,21 @@ export function activityTarget(type: string): ActivityTargetInfo | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Every homepage widget type any running plugin registered with
|
||||
* registerHomepageWidget, for the homepage plugin's own canvas and add-widget
|
||||
* menu. Reactive: it re-renders as plugins enable and disable.
|
||||
* Every item any running plugin added to `pointId` with registerExtension.
|
||||
* Reactive: it re-renders as plugins enable and disable.
|
||||
*/
|
||||
export function useHomepageWidgetTypes(): HomepageWidgetContribution[] {
|
||||
return requireHost().useHomepageWidgetTypes();
|
||||
export function useExtensions<T extends ExtensionContribution>(
|
||||
pointId: string,
|
||||
): T[] {
|
||||
return requireHost().useExtensions(pointId) as T[];
|
||||
}
|
||||
|
||||
/** A single registered homepage widget type, by id, or undefined if none. */
|
||||
export function homepageWidgetType(
|
||||
/** A single item added to `pointId`, by id, or undefined if none. */
|
||||
export function getExtension<T extends ExtensionContribution>(
|
||||
pointId: string,
|
||||
id: string,
|
||||
): HomepageWidgetContribution | undefined {
|
||||
return requireHost().homepageWidgetType(id);
|
||||
): T | undefined {
|
||||
return requireHost().getExtension(pointId, id) as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -129,8 +129,8 @@ const offline = () =>
|
||||
useHostActions: () => [],
|
||||
useActivityTypes: () => [],
|
||||
activityTarget: () => undefined,
|
||||
useHomepageWidgetTypes: () => [],
|
||||
homepageWidgetType: () => undefined,
|
||||
useExtensions: () => [],
|
||||
getExtension: () => undefined,
|
||||
usePluginUiPreferences: () => ({ values: {}, set: noop }),
|
||||
core: {
|
||||
logActivity: async () => {},
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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,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>>;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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", {
|
||||
|
||||
@@ -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>>;
|
||||
};
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
+11
-11
@@ -35,7 +35,7 @@ import {
|
||||
registerDashboardCard,
|
||||
type DashboardCardRenderProps,
|
||||
} from "@/dashboard/dashboard-cards-registry";
|
||||
import { registerHomepageWidgetType } from "./homepage-widget-registry";
|
||||
import { registerExtension } from "./extension-registry";
|
||||
import { registerSettingsComponent } from "@/settings/settings-components";
|
||||
import {
|
||||
declareActionSlot,
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
} from "@/shell/action-registry";
|
||||
import { pluginApiFor, pluginFetch, pluginWsUrl } from "@/lib/plugin-transport";
|
||||
import { registerPluginComponent } from "./component-registry";
|
||||
import type { RegisteredHomepageWidget } from "./homepage-widget-registry";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
registerLoginMethod,
|
||||
@@ -360,16 +359,17 @@ export function createPluginApp(
|
||||
);
|
||||
},
|
||||
|
||||
registerHomepageWidget(widget) {
|
||||
registerExtension(pointId, extension) {
|
||||
const components = extension.components
|
||||
? Object.fromEntries(
|
||||
Object.entries(extension.components).map(([name, component]) => [
|
||||
name,
|
||||
scoped(component),
|
||||
]),
|
||||
)
|
||||
: undefined;
|
||||
return track(
|
||||
registerHomepageWidgetType({
|
||||
...widget,
|
||||
pluginId,
|
||||
component: scoped(widget.component),
|
||||
editFormComponent: widget.editFormComponent
|
||||
? scoped(widget.editFormComponent)
|
||||
: undefined,
|
||||
} as unknown as RegisteredHomepageWidget),
|
||||
registerExtension(pointId, { ...extension, components, pluginId }),
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { enabledHostProtocols } from "@/sidebar/host-protocols";
|
||||
import { useHostActions } from "@/sidebar/host-contributions";
|
||||
import { tabTypeForActivity, useTabTypes } from "@/shell/tab-registry";
|
||||
import {
|
||||
getHomepageWidgetType,
|
||||
useHomepageWidgetTypes,
|
||||
} from "./homepage-widget-registry";
|
||||
import { getExtension, useExtensions } from "./extension-registry";
|
||||
import { useTranslation as useI18nTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
__setPluginHost,
|
||||
type HomepageWidgetContribution,
|
||||
type ExtensionContribution,
|
||||
type HostActionContribution,
|
||||
type PluginHostBridge,
|
||||
type PluginHostRecord,
|
||||
@@ -380,11 +377,9 @@ export const pluginHostBridge: PluginHostBridge = {
|
||||
return { icon: def.icon, tab: def.id, titleKey: def.titleKey };
|
||||
},
|
||||
|
||||
useHomepageWidgetTypes: () =>
|
||||
useHomepageWidgetTypes() as unknown as HomepageWidgetContribution[],
|
||||
homepageWidgetType: (id) =>
|
||||
getHomepageWidgetType(id) as unknown as
|
||||
HomepageWidgetContribution | undefined,
|
||||
useExtensions: (pointId) => useExtensions(pointId) as ExtensionContribution[],
|
||||
getExtension: (pointId, id) =>
|
||||
getExtension(pointId, id) as ExtensionContribution | undefined,
|
||||
|
||||
usePluginUiPreferences: (pluginId) => {
|
||||
const ctx = useUiPreferencesContext();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createRegistry, type Registry } from "@/lib/registry";
|
||||
|
||||
/**
|
||||
* Named lists any plugin can add to through app.registerExtension. The plugin
|
||||
* that reads a point decides its id and item shape; core only stores them, so
|
||||
* contributions survive the reader being off and core never learns what a
|
||||
* point is for.
|
||||
*/
|
||||
export interface RegisteredExtension {
|
||||
id: string;
|
||||
pluginId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const points = new Map<string, Registry<RegisteredExtension>>();
|
||||
|
||||
function point(id: string): Registry<RegisteredExtension> {
|
||||
let registry = points.get(id);
|
||||
if (!registry) {
|
||||
registry = createRegistry<RegisteredExtension>();
|
||||
points.set(id, registry);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
export function registerExtension(
|
||||
pointId: string,
|
||||
extension: RegisteredExtension,
|
||||
): () => void {
|
||||
return point(pointId).register(extension);
|
||||
}
|
||||
|
||||
export function getExtension(
|
||||
pointId: string,
|
||||
id: string,
|
||||
): RegisteredExtension | undefined {
|
||||
return point(pointId).get(id);
|
||||
}
|
||||
|
||||
export function listExtensions(pointId: string): RegisteredExtension[] {
|
||||
return point(pointId).list();
|
||||
}
|
||||
|
||||
export function useExtensions(pointId: string): RegisteredExtension[] {
|
||||
return point(pointId).useList();
|
||||
}
|
||||
|
||||
export function resetExtensions(): void {
|
||||
for (const registry of points.values()) registry.reset();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { createRegistry } from "@/lib/registry";
|
||||
|
||||
/**
|
||||
* A homepage widget type any plugin contributes through
|
||||
* app.registerHomepageWidget. Core owns this registry so the homepage
|
||||
* plugin's canvas can read it without every widget-contributing plugin
|
||||
* depending on homepage being enabled.
|
||||
*/
|
||||
export interface RegisteredHomepageWidget {
|
||||
id: string;
|
||||
pluginId?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: "links" | "info" | "system" | "monitoring";
|
||||
icon: React.ReactNode;
|
||||
defaultConfig: Record<string, unknown>;
|
||||
defaultSize: { w: number; h: number };
|
||||
minSize: { w: number; h: number };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
component: ComponentType<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editFormComponent?: ComponentType<any>;
|
||||
}
|
||||
|
||||
const registry = createRegistry<RegisteredHomepageWidget>();
|
||||
|
||||
export const registerHomepageWidgetType = registry.register;
|
||||
export const getHomepageWidgetType = registry.get;
|
||||
export const useHomepageWidgetTypes = registry.useList;
|
||||
export const resetHomepageWidgetTypes = registry.reset;
|
||||
@@ -3,9 +3,9 @@ import { useState } from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { Puzzle } from "lucide-react";
|
||||
import {
|
||||
homepageWidgetType,
|
||||
getExtension,
|
||||
useActivityTypes,
|
||||
useHomepageWidgetTypes,
|
||||
useExtensions,
|
||||
useHostActions,
|
||||
usePermission,
|
||||
useTranslation,
|
||||
@@ -39,7 +39,7 @@ import { resetRegisteredRailItems } from "@/sidebar/rail-items";
|
||||
import { resetTabTypes } from "@/shell/tab-registry";
|
||||
import { resetPanels } from "@/shell/panel-registry";
|
||||
import { resetDashboardCards } from "@/dashboard/dashboard-cards-registry";
|
||||
import { resetHomepageWidgetTypes } from "@/plugin-host/homepage-widget-registry";
|
||||
import { resetExtensions } from "@/plugin-host/extension-registry";
|
||||
import { resetSettingsComponents } from "@/settings/settings-components";
|
||||
import { resetActionRegistry } from "@/shell/action-registry";
|
||||
import { resetPluginStore } from "@/plugin-host/plugin-store";
|
||||
@@ -132,16 +132,11 @@ function activate(app: TermixApp) {
|
||||
),
|
||||
});
|
||||
app.registerSettingsComponent("probe", PermissionProbe);
|
||||
app.registerHomepageWidget({
|
||||
app.registerExtension("fixture.widgets", {
|
||||
id: "fixture-widget",
|
||||
name: "Fixture Widget",
|
||||
description: "A fixture widget",
|
||||
category: "info",
|
||||
icon: <Puzzle size={14} />,
|
||||
defaultConfig: {},
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
component: () => <span>fixture widget</span>,
|
||||
components: { view: () => <span>fixture widget</span> },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,7 +163,7 @@ afterEach(async () => {
|
||||
resetHostEditorSections();
|
||||
resetPaletteEntries();
|
||||
resetPluginStore();
|
||||
resetHomepageWidgetTypes();
|
||||
resetExtensions();
|
||||
});
|
||||
|
||||
const host = (overrides: Partial<Host> = {}) =>
|
||||
@@ -294,15 +289,33 @@ describe("registries through the app object", () => {
|
||||
expect(await screen.findByText(/fixture_activity/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("registers a homepage widget type readable through the SDK", async () => {
|
||||
it("adds an extension other plugins read by point id", async () => {
|
||||
await mount();
|
||||
expect(homepageWidgetType("fixture-widget")?.name).toBe("Fixture Widget");
|
||||
const widget = getExtension("fixture.widgets", "fixture-widget");
|
||||
expect(widget?.name).toBe("Fixture Widget");
|
||||
expect(widget?.pluginId).toBe("fixture");
|
||||
expect(getExtension("other.point", "fixture-widget")).toBeUndefined();
|
||||
const View = widget?.components?.view;
|
||||
function Probe() {
|
||||
const types = useHomepageWidgetTypes();
|
||||
return <span>{types.map((w) => w.id).join(",")}</span>;
|
||||
const items = useExtensions("fixture.widgets");
|
||||
return <span>{items.map((w) => w.id).join(",")}</span>;
|
||||
}
|
||||
render(<Probe />);
|
||||
render(
|
||||
<>
|
||||
<Probe />
|
||||
{View && <View />}
|
||||
</>,
|
||||
);
|
||||
expect(await screen.findByText("fixture-widget")).toBeTruthy();
|
||||
expect(await screen.findByText("fixture widget")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("drops a plugin's extensions when it deactivates", async () => {
|
||||
await mount();
|
||||
expect(getExtension("fixture.widgets", "fixture-widget")).toBeTruthy();
|
||||
await rendered?.deactivate();
|
||||
rendered = null;
|
||||
expect(getExtension("fixture.widgets", "fixture-widget")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("offers a per-host palette entry that runs against the shell", async () => {
|
||||
|
||||
Reference in new issue
Block a user