blob: 0219da3f7e3d87d9ba90c255989b651ae2d8e8d0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RefObject } from 'react';
import builtin from '@/plugins/builtin';
import * as allPlugins from '@/plugins';
import type * as Type from '@/common/interface';
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
import { getPluginsStatus } from '@/services';
import Storage from '@/utils/storage';
import request from '@/utils/request';
import { initI18nResource } from './utils';
import { Plugin, PluginInfo, PluginType } from './interface';
/**
* This information is to be defined for all components.
* It may be used for feature upgrades or version compatibility processing.
*
* @field slug_name: Unique identity string for the plugin, usually configured in `info.yaml`
* @field type: The type of plugin is defined and a single type of plugin can have multiple implementations.
* For example, a plugin of type `connector` can have a `google` implementation and a `github` implementation.
* `PluginRender` automatically renders the plug-in types already included in `PluginType`.
* @field name: Plugin name, optionally configurable. Usually read from the `i18n` file
* @field description: Plugin description, optionally configurable. Usually read from the `i18n` file
*/
class Plugins {
plugins: Plugin[] = [];
registeredPlugins: Type.ActivatedPlugin[] = [];
initialization: Promise<void>;
constructor() {
this.initialization = this.init();
}
async init() {
this.registerBuiltin();
// Note: The /install stage does not allow access to the getPluginsStatus api, so an initial value needs to be given
const plugins = (await getPluginsStatus().catch(() => [])) || [];
this.registeredPlugins = plugins.filter((p) => p.enabled);
await this.registerPlugins();
}
refresh() {
this.plugins = [];
this.init();
}
validate(plugin: Plugin) {
if (!plugin) {
return false;
}
const { info } = plugin;
const { slug_name, type } = info;
if (!slug_name) {
return false;
}
if (!type) {
return false;
}
return true;
}
registerBuiltin() {
Object.keys(builtin).forEach((key) => {
const plugin = builtin[key];
// builttin plugins are always activated
// Use own internal rendering logic'
plugin.activated = true;
this.register(plugin);
});
}
registerPlugins() {
const plugins = this.registeredPlugins
.map((p) => {
const func = allPlugins[p.slug_name];
return func;
})
.filter((p) => p);
return Promise.all(plugins.map((p) => p())).then((resolvedPlugins) => {
resolvedPlugins.forEach((plugin) => this.register(plugin));
return true;
});
}
register(plugin: Plugin) {
const bool = this.validate(plugin);
if (!bool) {
return;
}
if (plugin.i18nConfig) {
initI18nResource(plugin.i18nConfig);
}
plugin.activated = true;
this.plugins.push(plugin);
}
getPlugin(slug_name: string) {
return this.plugins.find((p) => p.info.slug_name === slug_name);
}
getOnePluginHooks(slug_name: string) {
const plugin = this.getPlugin(slug_name);
return plugin?.hooks;
}
getPlugins() {
return this.plugins;
}
}
const plugins = new Plugins();
const getRoutePlugins = async () => {
await plugins.initialization;
return plugins
.getPlugins()
.filter((plugin) => plugin.info.type === PluginType.Route);
};
const defaultProps = () => {
const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || '';
return {
key: token,
headers: {
Authorization: token,
},
};
};
const validateRoutePlugin = async (slugName) => {
let registeredPlugin;
if (plugins.registeredPlugins.length === 0) {
const pluginsStatus = await getPluginsStatus();
registeredPlugin = pluginsStatus.find((p) => p.slug_name === slugName);
} else {
registeredPlugin = plugins.registeredPlugins.find(
(p) => p.slug_name === slugName,
);
}
return Boolean(registeredPlugin?.enabled);
};
const mergeRoutePlugins = async (routes) => {
const routePlugins = await getRoutePlugins();
if (routePlugins.length === 0) {
return routes;
}
routes.forEach((route) => {
if (route.page === 'pages/Layout') {
route.children?.forEach((child) => {
if (child.page === 'pages/SideNavLayout') {
routePlugins.forEach((plugin) => {
const { route: path, slug_name } = plugin.info;
child.children.push({
page: plugin.component,
path,
loader: async () => {
const bool = await validateRoutePlugin(slug_name);
return bool;
},
guard: (params) => {
if (params.loaderData) {
return {
ok: true,
};
}
return {
ok: false,
error: {
code: 404,
},
};
},
});
});
}
});
}
});
return routes;
};
/**
* Only used to enhance the capabilities of the markdown editor
* Add RefObject type to solve the problem of dom being null in hooks
*/
const useRenderHtmlPlugin = (
element: HTMLElement | RefObject<HTMLElement> | null,
) => {
plugins
.getPlugins()
.filter((plugin) => {
return (
plugin.activated &&
plugin.hooks?.useRender &&
(plugin.info.type === PluginType.Editor ||
plugin.info.type === PluginType.Render)
);
})
.forEach((plugin) => {
plugin.hooks?.useRender?.forEach((hook) => {
hook(element, request);
});
});
};
// Only for render type plugins
const useRenderPlugin = (
element: HTMLElement | RefObject<HTMLElement> | null,
) => {
return plugins
.getPlugins()
.filter((plugin) => {
return (
plugin.activated &&
plugin.hooks?.useRender &&
plugin.info.type === PluginType.Render
);
})
.forEach((plugin) => {
plugin.hooks?.useRender?.forEach((hook) => {
hook(element, request);
});
});
};
// Only one captcha type plug-in can be enabled at the same time
const useCaptchaPlugin = (key: Type.CaptchaKey) => {
const captcha = plugins
.getPlugins()
.filter(
(plugin) => plugin.info.type === PluginType.Captcha && plugin.activated,
);
const pluginHooks = plugins.getOnePluginHooks(captcha[0]?.info.slug_name);
return pluginHooks?.useCaptcha?.({
captchaKey: key,
commonProps: defaultProps(),
});
};
export type { Plugin, PluginInfo };
export {
useRenderHtmlPlugin,
mergeRoutePlugins,
useCaptchaPlugin,
useRenderPlugin,
PluginType,
};
export default plugins;