chore: Refactor client plugin management (#7053)
* Update clientside plugin management to work as server * docs * tsc * Rebase main
This commit is contained in:
@@ -1,29 +1,37 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import PluginLoader from "~/utils/PluginLoader";
|
import Logger from "~/utils/Logger";
|
||||||
|
import { Hook, usePluginValue } from "~/utils/PluginManager";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
/** The ID of the plugin to render an Icon for. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** The size of the icon. */
|
||||||
size?: number;
|
size?: number;
|
||||||
|
/** The color of the icon. */
|
||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an icon defined in a plugin (Hook.Icon).
|
||||||
|
*/
|
||||||
function PluginIcon({ id, color, size = 24 }: Props) {
|
function PluginIcon({ id, color, size = 24 }: Props) {
|
||||||
const plugin = PluginLoader.plugins[id];
|
const Icon = usePluginValue(Hook.Icon, id);
|
||||||
const Icon = plugin?.icon;
|
|
||||||
|
|
||||||
if (Icon) {
|
if (Icon) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<IconPosition>
|
||||||
<Icon size={size} fill={color} />
|
<Icon size={size} fill={color} />
|
||||||
</Wrapper>
|
</IconPosition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.warn("No Icon registered for plugin", { id });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const IconPosition = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -32,4 +40,4 @@ const Wrapper = styled.div`
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default PluginIcon;
|
export default observer(PluginIcon);
|
||||||
|
|||||||
16
app/hooks/useComputed.ts
Normal file
16
app/hooks/useComputed.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { computed } from "mobx";
|
||||||
|
import { type DependencyList, useMemo } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook around MobX computed function that runs computation whenever observable values change.
|
||||||
|
*
|
||||||
|
* @param callback Function which returns a memorized value.
|
||||||
|
* @param inputs Dependency list for useMemo.
|
||||||
|
*/
|
||||||
|
export function useComputed<T>(
|
||||||
|
callback: () => T,
|
||||||
|
inputs: DependencyList = []
|
||||||
|
): T {
|
||||||
|
const value = useMemo(() => computed(callback), inputs);
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import sortBy from "lodash/sortBy";
|
|
||||||
import {
|
import {
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
ProfileIcon,
|
ProfileIcon,
|
||||||
@@ -20,10 +19,11 @@ import React, { ComponentProps } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||||
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
||||||
import PluginLoader from "~/utils/PluginLoader";
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
import isCloudHosted from "~/utils/isCloudHosted";
|
import isCloudHosted from "~/utils/isCloudHosted";
|
||||||
import lazy from "~/utils/lazyWithRetry";
|
import lazy from "~/utils/lazyWithRetry";
|
||||||
import { settingsPath } from "~/utils/routeHelpers";
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
|
import { useComputed } from "./useComputed";
|
||||||
import useCurrentTeam from "./useCurrentTeam";
|
import useCurrentTeam from "./useCurrentTeam";
|
||||||
import useCurrentUser from "./useCurrentUser";
|
import useCurrentUser from "./useCurrentUser";
|
||||||
import usePolicy from "./usePolicy";
|
import usePolicy from "./usePolicy";
|
||||||
@@ -59,7 +59,7 @@ const useSettingsConfig = () => {
|
|||||||
const can = usePolicy(team);
|
const can = usePolicy(team);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const config = React.useMemo(() => {
|
const config = useComputed(() => {
|
||||||
const items: ConfigItem[] = [
|
const items: ConfigItem[] = [
|
||||||
// Account
|
// Account
|
||||||
{
|
{
|
||||||
@@ -187,37 +187,19 @@ const useSettingsConfig = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
const insertIndex = items.findIndex((i) => i.group === t("Integrations"));
|
PluginManager.getHooks(Hook.Settings).forEach((plugin) => {
|
||||||
items.splice(
|
const insertIndex = items.findIndex(
|
||||||
insertIndex,
|
(i) => i.group === t(plugin.value.group ?? "Integrations")
|
||||||
0,
|
|
||||||
...(sortBy(
|
|
||||||
Object.values(PluginLoader.plugins),
|
|
||||||
(plugin) => plugin.config?.priority ?? 0
|
|
||||||
).map((plugin) => {
|
|
||||||
const hasSettings = !!plugin.settings;
|
|
||||||
const enabledInDeployment =
|
|
||||||
!plugin.config?.deployments ||
|
|
||||||
plugin.config.deployments.length === 0 ||
|
|
||||||
(plugin.config.deployments.includes("community") && !isCloudHosted) ||
|
|
||||||
(plugin.config.deployments.includes("cloud") && isCloudHosted) ||
|
|
||||||
(plugin.config.deployments.includes("enterprise") && !isCloudHosted);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: t(plugin.config.name),
|
|
||||||
path: integrationSettingsPath(plugin.id),
|
|
||||||
// TODO: Remove hardcoding of plugin id here
|
|
||||||
group:
|
|
||||||
plugin.id === "collections" ? t("Workspace") : t("Integrations"),
|
|
||||||
component: plugin.settings,
|
|
||||||
enabled:
|
|
||||||
enabledInDeployment &&
|
|
||||||
hasSettings &&
|
|
||||||
(plugin.config.roles?.includes(user.role) || can.update),
|
|
||||||
icon: plugin.icon,
|
|
||||||
};
|
|
||||||
}) as ConfigItem[])
|
|
||||||
);
|
);
|
||||||
|
items.splice(insertIndex, 0, {
|
||||||
|
name: t(plugin.name),
|
||||||
|
path: integrationSettingsPath(plugin.id),
|
||||||
|
group: t(plugin.value.group),
|
||||||
|
component: plugin.value.component,
|
||||||
|
enabled: plugin.roles?.includes(user.role) || can.update,
|
||||||
|
icon: plugin.value.icon,
|
||||||
|
} as ConfigItem);
|
||||||
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [t, can.createApiKey, can.update, can.createImport, can.createExport]);
|
}, [t, can.createApiKey, can.update, can.createImport, can.createExport]);
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ import LazyPolyfill from "./components/LazyPolyfills";
|
|||||||
import PageScroll from "./components/PageScroll";
|
import PageScroll from "./components/PageScroll";
|
||||||
import Routes from "./routes";
|
import Routes from "./routes";
|
||||||
import Logger from "./utils/Logger";
|
import Logger from "./utils/Logger";
|
||||||
|
import { PluginManager } from "./utils/PluginManager";
|
||||||
import history from "./utils/history";
|
import history from "./utils/history";
|
||||||
import { initSentry } from "./utils/sentry";
|
import { initSentry } from "./utils/sentry";
|
||||||
|
|
||||||
|
// Load plugins as soon as possible
|
||||||
|
void PluginManager.loadPlugins();
|
||||||
|
|
||||||
initI18n(env.DEFAULT_LANGUAGE);
|
initI18n(env.DEFAULT_LANGUAGE);
|
||||||
const element = window.document.getElementById("root");
|
const element = window.document.getElementById("root");
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ function AuthenticationProvider(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
|
||||||
<ButtonLarge
|
<ButtonLarge
|
||||||
onClick={() => (window.location.href = href)}
|
onClick={() => (window.location.href = href)}
|
||||||
icon={<PluginIcon id={id} />}
|
icon={<PluginIcon id={id} />}
|
||||||
@@ -103,7 +102,6 @@ function AuthenticationProvider(props: Props) {
|
|||||||
authProviderName: name,
|
authProviderName: name,
|
||||||
})}
|
})}
|
||||||
</ButtonLarge>
|
</ButtonLarge>
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ type LogCategory =
|
|||||||
| "editor"
|
| "editor"
|
||||||
| "router"
|
| "router"
|
||||||
| "collaboration"
|
| "collaboration"
|
||||||
| "misc";
|
| "misc"
|
||||||
|
| "plugins";
|
||||||
|
|
||||||
type Extra = Record<string, any>;
|
type Extra = Record<string, any>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { UserRole } from "@shared/types";
|
|
||||||
|
|
||||||
interface Plugin {
|
|
||||||
id: string;
|
|
||||||
config: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
roles?: UserRole[];
|
|
||||||
deployments?: string[];
|
|
||||||
priority?: number;
|
|
||||||
};
|
|
||||||
settings: React.FC;
|
|
||||||
icon: React.FC<{ size?: number; fill?: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PluginLoader {
|
|
||||||
private static pluginsCache: { [id: string]: Plugin };
|
|
||||||
|
|
||||||
public static get plugins(): { [id: string]: Plugin } {
|
|
||||||
if (this.pluginsCache) {
|
|
||||||
return this.pluginsCache;
|
|
||||||
}
|
|
||||||
const plugins = {};
|
|
||||||
|
|
||||||
function importAll(r: any, property: string) {
|
|
||||||
Object.keys(r).forEach((key: string) => {
|
|
||||||
const id = key.split("/")[3];
|
|
||||||
plugins[id] = plugins[id] || {
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
plugins[id][property] = r[key].default ?? React.lazy(r[key]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
importAll(
|
|
||||||
import.meta.glob("../../plugins/*/client/Settings.{ts,js,tsx,jsx}"),
|
|
||||||
"settings"
|
|
||||||
);
|
|
||||||
importAll(
|
|
||||||
import.meta.glob("../../plugins/*/client/Icon.{ts,js,tsx,jsx}", {
|
|
||||||
eager: true,
|
|
||||||
}),
|
|
||||||
"icon"
|
|
||||||
);
|
|
||||||
importAll(
|
|
||||||
import.meta.glob("../../plugins/*/plugin.json", { eager: true }),
|
|
||||||
"config"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.pluginsCache = plugins;
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
145
app/utils/PluginManager.ts
Normal file
145
app/utils/PluginManager.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import isArray from "lodash/isArray";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
|
import { action, observable } from "mobx";
|
||||||
|
import { useComputed } from "~/hooks/useComputed";
|
||||||
|
import Logger from "./Logger";
|
||||||
|
import isCloudHosted from "./isCloudHosted";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different types of client plugins that can be registered.
|
||||||
|
*/
|
||||||
|
export enum Hook {
|
||||||
|
Settings = "settings",
|
||||||
|
Icon = "icon",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of plugin types to their values, each plugin type has a different shape of value.
|
||||||
|
*/
|
||||||
|
type PluginValueMap = {
|
||||||
|
[Hook.Settings]: {
|
||||||
|
/** The group in settings sidebar this plugin belongs to. */
|
||||||
|
group: string;
|
||||||
|
/** The displayed icon of the plugin. */
|
||||||
|
icon: React.ElementType;
|
||||||
|
/** The settings screen somponent, should be lazy loaded. */
|
||||||
|
component: React.LazyExoticComponent<React.ComponentType>;
|
||||||
|
};
|
||||||
|
[Hook.Icon]: React.ElementType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Plugin<T extends Hook> = {
|
||||||
|
/** A unique identifier for the plugin */
|
||||||
|
id: string;
|
||||||
|
/** Plugin type */
|
||||||
|
type: T;
|
||||||
|
/** The plugin's display name */
|
||||||
|
name: string;
|
||||||
|
/** A brief description of the plugin */
|
||||||
|
description?: string;
|
||||||
|
/** The plugin content */
|
||||||
|
value: PluginValueMap[T];
|
||||||
|
/** Priority will affect order in menus and execution. Lower is earlier. */
|
||||||
|
priority?: number;
|
||||||
|
/** The deployments this plugin is enabled for (default: all) */
|
||||||
|
deployments?: string[];
|
||||||
|
/** The roles this plugin is enabled for. (default: admin) */
|
||||||
|
roles?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client plugin manager.
|
||||||
|
*/
|
||||||
|
export class PluginManager {
|
||||||
|
/**
|
||||||
|
* Add plugins to the manager.
|
||||||
|
*
|
||||||
|
* @param plugins
|
||||||
|
*/
|
||||||
|
public static add(plugins: Array<Plugin<Hook>> | Plugin<Hook>) {
|
||||||
|
if (isArray(plugins)) {
|
||||||
|
return plugins.forEach((plugin) => this.register(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.register(plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
private static register<T extends Hook>(plugin: Plugin<T>) {
|
||||||
|
const enabledInDeployment =
|
||||||
|
!plugin?.deployments ||
|
||||||
|
plugin.deployments.length === 0 ||
|
||||||
|
(plugin.deployments.includes("cloud") && isCloudHosted) ||
|
||||||
|
(plugin.deployments.includes("community") && !isCloudHosted) ||
|
||||||
|
(plugin.deployments.includes("enterprise") && !isCloudHosted);
|
||||||
|
if (!enabledInDeployment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.plugins.has(plugin.type)) {
|
||||||
|
this.plugins.set(plugin.type, observable.array([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugins
|
||||||
|
.get(plugin.type)!
|
||||||
|
.push({ ...plugin, priority: plugin.priority ?? 0 });
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
"plugins",
|
||||||
|
`Plugin(type=${plugin.type}) registered ${plugin.name} ${
|
||||||
|
plugin.description ? `(${plugin.description})` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the plugins of a given type in order of priority.
|
||||||
|
*
|
||||||
|
* @param type The type of plugin to filter by
|
||||||
|
* @returns A list of plugins
|
||||||
|
*/
|
||||||
|
public static getHooks<T extends Hook>(type: T) {
|
||||||
|
return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a plugin of a given type by its id.
|
||||||
|
*
|
||||||
|
* @param type The type of plugin to filter by
|
||||||
|
* @param id The id of the plugin
|
||||||
|
* @returns A plugin
|
||||||
|
*/
|
||||||
|
public static getHook<T extends Hook>(type: T, id: string) {
|
||||||
|
return this.plugins.get(type)?.find((hook) => hook.id === id) as
|
||||||
|
| Plugin<T>
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load plugin client components, must be in `/<plugin>/client/index.ts(x)`
|
||||||
|
*/
|
||||||
|
public static async loadPlugins() {
|
||||||
|
if (this.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = import.meta.glob("../../plugins/*/client/index.{ts,js,tsx,jsx}");
|
||||||
|
await Promise.all(Object.keys(r).map((key: string) => r[key]()));
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static plugins = observable.map<Hook, Plugin<Hook>[]>();
|
||||||
|
|
||||||
|
@observable
|
||||||
|
private static loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience hook to get the value for a specific plugin and type.
|
||||||
|
*/
|
||||||
|
export function usePluginValue<T extends Hook>(type: T, id: string) {
|
||||||
|
return useComputed(
|
||||||
|
() => PluginManager.getHook<T>(type, id)?.value,
|
||||||
|
[type, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
11
plugins/azure/client/index.tsx
Normal file
11
plugins/azure/client/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Icon,
|
||||||
|
value: Icon,
|
||||||
|
},
|
||||||
|
]);
|
||||||
11
plugins/discord/client/index.tsx
Normal file
11
plugins/discord/client/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Icon,
|
||||||
|
value: Icon,
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
plugins/github/client/index.tsx
Normal file
16
plugins/github/client/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Settings,
|
||||||
|
value: {
|
||||||
|
group: "Integrations",
|
||||||
|
icon: Icon,
|
||||||
|
component: React.lazy(() => import("./Settings")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
11
plugins/google/client/index.tsx
Normal file
11
plugins/google/client/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Icon,
|
||||||
|
value: Icon,
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
plugins/googleanalytics/client/index.tsx
Normal file
16
plugins/googleanalytics/client/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Settings,
|
||||||
|
value: {
|
||||||
|
group: "Integrations",
|
||||||
|
icon: Icon,
|
||||||
|
component: React.lazy(() => import("./Settings")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
plugins/matomo/client/index.tsx
Normal file
16
plugins/matomo/client/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Settings,
|
||||||
|
value: {
|
||||||
|
group: "Integrations",
|
||||||
|
icon: Icon,
|
||||||
|
component: React.lazy(() => import("./Settings")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
21
plugins/slack/client/index.tsx
Normal file
21
plugins/slack/client/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Settings,
|
||||||
|
value: {
|
||||||
|
group: "Integrations",
|
||||||
|
icon: Icon,
|
||||||
|
component: React.lazy(() => import("./Settings")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Icon,
|
||||||
|
value: Icon,
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
plugins/webhooks/client/index.tsx
Normal file
16
plugins/webhooks/client/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||||
|
import config from "../plugin.json";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
PluginManager.add([
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
type: Hook.Settings,
|
||||||
|
value: {
|
||||||
|
group: "Integrations",
|
||||||
|
icon: Icon,
|
||||||
|
component: React.lazy(() => import("./Settings")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -58,10 +58,15 @@ export type Plugin<T extends Hook> = {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server plugin manager.
|
||||||
|
*/
|
||||||
export class PluginManager {
|
export class PluginManager {
|
||||||
private static plugins = new Map<Hook, Plugin<Hook>[]>();
|
private static plugins = new Map<Hook, Plugin<Hook>[]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add plugins
|
* Add plugins to the manager.
|
||||||
|
*
|
||||||
* @param plugins
|
* @param plugins
|
||||||
*/
|
*/
|
||||||
public static add(plugins: Array<Plugin<Hook>> | Plugin<Hook>) {
|
public static add(plugins: Array<Plugin<Hook>> | Plugin<Hook>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user