mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-16 17:56:30 +00:00
Plugin source URLs (#10000)
* Better handling of URLs when loading plugin source - Handle complex URLs more cleanly - Support loading from actual external host - Support loading with specified port * Fix URL rendering - handle "local" and "remote" components * Use default host if not provided * Simplify code
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { generateUrl } from '../../functions/urls';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
||||
/*
|
||||
* Load an external plugin source from a URL.
|
||||
@@ -32,13 +33,22 @@ export async function findExternalPluginFunction(
|
||||
source: string,
|
||||
functionName: string
|
||||
): Promise<Function | null> {
|
||||
// The source URL may also include the function name divided by a colon
|
||||
// otherwise the provided function name will be used
|
||||
if (source.includes(':')) {
|
||||
[source, functionName] = source.split(':');
|
||||
const { getHost } = useLocalState.getState();
|
||||
|
||||
// Extract pathstring from the source URL
|
||||
// Use the specified host unless the source is already a full URL
|
||||
const url = new URL(source, getHost());
|
||||
|
||||
// If the pathname contains a ':' character, it indicates a function name
|
||||
// but we need to remove it for the URL lookup to work correctly
|
||||
if (url.pathname.includes(':')) {
|
||||
const parts = url.pathname.split(':');
|
||||
source = parts[0]; // Use the first part as the source URL
|
||||
functionName = parts[1] || functionName; // Use the second part as the
|
||||
url.pathname = source; // Update the pathname to the source URL
|
||||
}
|
||||
|
||||
const module = await loadExternalPluginSource(source);
|
||||
const module = await loadExternalPluginSource(url.toString());
|
||||
|
||||
if (module?.[functionName]) {
|
||||
return module[functionName];
|
||||
|
@@ -9,6 +9,7 @@ import { type Root, createRoot } from 'react-dom/client';
|
||||
import { api, queryClient } from '../../App';
|
||||
import { ApiProvider } from '../../contexts/ApiContext';
|
||||
import { LanguageContext } from '../../contexts/LanguageContext';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { findExternalPluginFunction } from './PluginSource';
|
||||
|
||||
@@ -43,19 +44,17 @@ export default function RemoteComponent({
|
||||
undefined
|
||||
);
|
||||
|
||||
const sourceFile = useMemo(() => {
|
||||
return source.split(':')[0];
|
||||
}, [source]);
|
||||
const func: string = useMemo(() => {
|
||||
// Attempt to extract the function name from the source
|
||||
const { getHost } = useLocalState.getState();
|
||||
const url = new URL(source, getHost());
|
||||
|
||||
// Determine the function to call in the external plugin source
|
||||
const functionName = useMemo(() => {
|
||||
// The "source" string may contain a function name, e.g. "source.js:myFunction"
|
||||
if (source.includes(':')) {
|
||||
return source.split(':')[1];
|
||||
if (url.pathname.includes(':')) {
|
||||
const parts = url.pathname.split(':');
|
||||
return parts[1] || defaultFunctionName; // Use the second part as the function name, or fallback to default
|
||||
} else {
|
||||
return defaultFunctionName;
|
||||
}
|
||||
|
||||
// By default, return the default function name
|
||||
return defaultFunctionName;
|
||||
}, [source, defaultFunctionName]);
|
||||
|
||||
const reloadPluginContent = useCallback(() => {
|
||||
@@ -68,8 +67,8 @@ export default function RemoteComponent({
|
||||
reloadContent: reloadPluginContent
|
||||
};
|
||||
|
||||
if (sourceFile && functionName) {
|
||||
findExternalPluginFunction(sourceFile, functionName)
|
||||
if (source && defaultFunctionName) {
|
||||
findExternalPluginFunction(source, func)
|
||||
.then((func) => {
|
||||
if (!!func) {
|
||||
try {
|
||||
@@ -99,30 +98,28 @@ export default function RemoteComponent({
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
setRenderingError(`${sourceFile}:${functionName}`);
|
||||
setRenderingError(`${source} / ${func}`);
|
||||
}
|
||||
})
|
||||
.catch((_error) => {
|
||||
console.error(
|
||||
`ERR: Failed to load remove plugin function: ${sourceFile}:${functionName}`
|
||||
`ERR: Failed to load remote plugin function: ${source} /${func}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setRenderingError(
|
||||
`${t`Invalid source or function name`} - ${sourceFile}:${functionName}`
|
||||
`${t`Invalid source or function name`} - ${source} /${func}`
|
||||
);
|
||||
}
|
||||
}, [componentRef, rootElement, sourceFile, functionName, context]);
|
||||
}, [componentRef, rootElement, source, defaultFunctionName, context]);
|
||||
|
||||
// Reload the plugin content dynamically
|
||||
useEffect(() => {
|
||||
reloadPluginContent();
|
||||
}, [sourceFile, functionName, context, rootElement]);
|
||||
}, [source, func, context, rootElement]);
|
||||
|
||||
return (
|
||||
<Boundary
|
||||
label={identifierString(`RemoteComponent-${sourceFile}-${functionName}`)}
|
||||
>
|
||||
<Boundary label={identifierString(`RemoteComponent-${func}`)}>
|
||||
<Stack gap='xs'>
|
||||
{renderingError && (
|
||||
<Alert
|
||||
|
Reference in New Issue
Block a user