2
0
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:
Oliver
2025-07-11 13:58:16 +10:00
committed by GitHub
parent 4d446198b6
commit 88f05ce434
2 changed files with 33 additions and 26 deletions

View File

@@ -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];

View File

@@ -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