mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
More work needed...
This commit is contained in:
parent
1dfad61c52
commit
b5f440df3f
@ -1,7 +1,7 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Alert, Text } from '@mantine/core';
|
import { Alert, Loader, LoadingOverlay, Text } from '@mantine/core';
|
||||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
@ -13,10 +13,13 @@ import { PluginElementProps } from './PluginElement';
|
|||||||
|
|
||||||
interface PluginPanelProps extends PanelType {
|
interface PluginPanelProps extends PanelType {
|
||||||
source?: string;
|
source?: string;
|
||||||
|
renderFunction?: string;
|
||||||
|
hiddenFunction?: string;
|
||||||
params?: any;
|
params?: any;
|
||||||
instance?: any;
|
instance?: any;
|
||||||
model?: ModelType | string;
|
model?: ModelType | string;
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
|
pluginKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder content for a panel with no content
|
// Placeholder content for a panel with no content
|
||||||
@ -28,6 +31,19 @@ function PanelNoContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placeholder content for a panel which has failed to load
|
||||||
|
function PanelErrorContent({ error }: { error: ReactNode | undefined }) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
title={t`Error Loading Plugin`}
|
||||||
|
icon={<IconExclamationCircle />}
|
||||||
|
>
|
||||||
|
<Text>{error}</Text>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Provide more context information to the plugin renderer:
|
* TODO: Provide more context information to the plugin renderer:
|
||||||
*
|
*
|
||||||
@ -49,52 +65,34 @@ function PanelNoContent() {
|
|||||||
* - `element` is the HTML element to render the content into
|
* - `element` is the HTML element to render the content into
|
||||||
* - `params` is the set of run-time parameters to pass to the content rendering function
|
* - `params` is the set of run-time parameters to pass to the content rendering function
|
||||||
*/
|
*/
|
||||||
export default function PluginPanel({ props }: { props: PluginPanelProps }) {
|
export default function PluginPanel(props: PluginPanelProps): PanelType {
|
||||||
const ref = useRef<HTMLDivElement>();
|
return {
|
||||||
|
...props,
|
||||||
|
content: `Hello world, from ${props.pluginKey}: ${props.name}`
|
||||||
|
};
|
||||||
|
|
||||||
const host = useLocalState((s) => s.host);
|
// TODO: Get this working!
|
||||||
|
// Note: Having these hooks below the "return" above causes errors,
|
||||||
|
// Warning: React has detected a change in the order of Hooks called by BasePanelGroup. This will lead to bugs and errors if not fixed
|
||||||
|
|
||||||
|
/*
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [ isLoading, setIsLoading ] = useState<boolean>(false);
|
||||||
const [errorDetail, setErrorDetail] = useState<ReactNode | undefined>(
|
const [errorDetail, setErrorDetail] = useState<ReactNode | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadExternalSource = async () => {
|
// External module which defines the plugin content
|
||||||
let source: string = props.source ?? '';
|
const [ pluginModule, setPluginModule ] = useState<any>(null);
|
||||||
|
|
||||||
if (!source) {
|
const host = useLocalState.getState().host;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.startsWith('/')) {
|
// Attributes to pass through to the plugin module
|
||||||
// Prefix the source with the host URL
|
const attributes : PluginElementProps= useMemo(() => {
|
||||||
source = `${host}${source}`;
|
return {
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Gate where this content may be loaded from (e.g. only allow certain domains)
|
|
||||||
|
|
||||||
let loaded: boolean = false;
|
|
||||||
|
|
||||||
// Load content from external source
|
|
||||||
const module = await import(/* @vite-ignore */ source ?? '')
|
|
||||||
.catch((error) => {
|
|
||||||
setErrorDetail(error.toString());
|
|
||||||
})
|
|
||||||
.then((module) => {
|
|
||||||
loaded = true;
|
|
||||||
return module;
|
|
||||||
});
|
|
||||||
|
|
||||||
// We expect the external source to define a function which will render the content
|
|
||||||
if (
|
|
||||||
loaded &&
|
|
||||||
module &&
|
|
||||||
module.render_panel &&
|
|
||||||
typeof module.render_panel === 'function'
|
|
||||||
) {
|
|
||||||
// Set of attributes to pass through to the plugin for rendering
|
|
||||||
let attributes: PluginElementProps = {
|
|
||||||
target: ref.current,
|
target: ref.current,
|
||||||
model: props.model,
|
model: props.model,
|
||||||
id: props.id,
|
id: props.id,
|
||||||
@ -104,38 +102,121 @@ export default function PluginPanel({ props }: { props: PluginPanelProps }) {
|
|||||||
host: host,
|
host: host,
|
||||||
navigate: navigate
|
navigate: navigate
|
||||||
};
|
};
|
||||||
|
}, [props, ref.current, api, user, navigate]);
|
||||||
|
|
||||||
module.render_panel(attributes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
// Reload external source if the source URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setErrorDetail(undefined);
|
loadExternalSource(props.source || '');
|
||||||
|
}, [props.source]);
|
||||||
|
|
||||||
if (props.source) {
|
// Memoize the panel rendering function
|
||||||
// Load content from external source
|
const renderPanel = useMemo(() => {
|
||||||
loadExternalSource();
|
const renderFunction = props.renderFunction || 'renderPanel';
|
||||||
} else if (props.content) {
|
|
||||||
// If content is provided directly, render it into the panel
|
|
||||||
if (ref) {
|
|
||||||
ref.current?.setHTMLUnsafe(props.content.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
if (errorDetail) {
|
console.log("renderPanel memo:", renderFunction, pluginModule);
|
||||||
return (
|
|
||||||
<Alert
|
if (pluginModule && pluginModule[renderFunction]) {
|
||||||
color="red"
|
return pluginModule[renderFunction];
|
||||||
title={t`Error Loading Plugin`}
|
}
|
||||||
icon={<IconExclamationCircle />}
|
return null;
|
||||||
>
|
}, [pluginModule, props.renderFunction]);
|
||||||
{errorDetail}
|
|
||||||
</Alert>
|
// Memoize if the panel is hidden
|
||||||
);
|
const isPanelHidden : boolean = useMemo(() => {
|
||||||
|
const hiddenFunction = props.hiddenFunction || 'isPanelHidden';
|
||||||
|
|
||||||
|
console.log("hiddenFunction memo:", hiddenFunction, pluginModule);
|
||||||
|
console.log("- attributes:", attributes);
|
||||||
|
|
||||||
|
if (pluginModule && pluginModule[hiddenFunction]) {
|
||||||
|
return !!pluginModule[hiddenFunction](attributes);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [attributes, pluginModule, props.hiddenFunction]);
|
||||||
|
|
||||||
|
// Construct the panel content
|
||||||
|
const panelContent = useMemo(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader />;
|
||||||
|
} else if (errorDetail) {
|
||||||
|
return <PanelErrorContent error={errorDetail} />;
|
||||||
} else if (!props.content && !props.source) {
|
} else if (!props.content && !props.source) {
|
||||||
return <PanelNoContent />;
|
return <PanelNoContent />;
|
||||||
} else {
|
} else {
|
||||||
return <div ref={ref as any}></div>;
|
return <div ref={ref as any}></div>;
|
||||||
}
|
}
|
||||||
|
}, [props, errorDetail, isLoading]);
|
||||||
|
|
||||||
|
// Regenerate the panel content as required
|
||||||
|
useEffect(() => {
|
||||||
|
// If a panel rendering function is provided, use that
|
||||||
|
|
||||||
|
console.log("Regenerating panel content...");
|
||||||
|
|
||||||
|
if (renderPanel) {
|
||||||
|
console.log("- using external rendering function");
|
||||||
|
renderPanel(attributes);
|
||||||
|
} else if (props.content) {
|
||||||
|
// If content is provided directly, render it into the panel
|
||||||
|
console.log("- using direct content");
|
||||||
|
if (ref) {
|
||||||
|
ref.current?.setHTMLUnsafe(props.content.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ref, renderPanel, props.content, attributes]);
|
||||||
|
|
||||||
|
// Load external source content
|
||||||
|
const loadExternalSource = async (source: string) => {
|
||||||
|
|
||||||
|
const host = useLocalState.getState().host;
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
setErrorDetail(undefined);
|
||||||
|
setPluginModule(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.startsWith('/')) {
|
||||||
|
// Prefix the source with the host URL
|
||||||
|
source = `${host}${source}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Gate where this content may be loaded from (e.g. only allow certain domains)
|
||||||
|
// TODO: Add a timeout for loading external content
|
||||||
|
// TODO: Restrict to certain file types (e.g. .js)
|
||||||
|
|
||||||
|
let errorMessage : ReactNode = undefined;
|
||||||
|
|
||||||
|
console.log("Loading plugin module from:", source);
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Load content from external source
|
||||||
|
const module = await import(/* @vite-ignore * / source ?? '')
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.toString();
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then((module) => {
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
console.log("Loaded plugin module:", module);
|
||||||
|
|
||||||
|
setPluginModule(module);
|
||||||
|
setErrorDetail(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the panel state
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
content: panelContent,
|
||||||
|
hidden: isPanelHidden,
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Alert, Text } from '@mantine/core';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@ -9,14 +8,10 @@ import PluginPanel from '../components/plugins/PluginPanel';
|
|||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { identifierString } from '../functions/conversion';
|
import { identifierString } from '../functions/conversion';
|
||||||
import { InvenTreeIcon } from '../functions/icons';
|
import { InvenTreeIcon, InvenTreeIconType } from '../functions/icons';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
|
|
||||||
export type PluginPanelState = {
|
|
||||||
panels: PanelType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function usePluginPanels({
|
export function usePluginPanels({
|
||||||
instance,
|
instance,
|
||||||
model,
|
model,
|
||||||
@ -25,16 +20,16 @@ export function usePluginPanels({
|
|||||||
instance?: any;
|
instance?: any;
|
||||||
model?: ModelType | string;
|
model?: ModelType | string;
|
||||||
id?: string | number | null;
|
id?: string | number | null;
|
||||||
}): PluginPanelState {
|
}): PanelType[] {
|
||||||
const globalSettings = useGlobalSettingsState();
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
const pluginPanelsEnabled = useMemo(
|
const pluginPanelsEnabled: boolean = useMemo(
|
||||||
() => globalSettings.isSet('ENABLE_PLUGINS_INTERFACE'),
|
() => globalSettings.isSet('ENABLE_PLUGINS_INTERFACE'),
|
||||||
[globalSettings]
|
[globalSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
// API query to fetch initial information on available plugin panels
|
// API query to fetch initial information on available plugin panels
|
||||||
const { isFetching, data } = useQuery({
|
const { isFetching, data: pluginPanels } = useQuery({
|
||||||
enabled: pluginPanelsEnabled && !!model && id != undefined,
|
enabled: pluginPanelsEnabled && !!model && id != undefined,
|
||||||
queryKey: [model, id],
|
queryKey: [model, id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@ -57,30 +52,22 @@ export function usePluginPanels({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const panels: PanelType[] = useMemo(() => {
|
|
||||||
return (
|
return (
|
||||||
data?.map((panel: any) => {
|
pluginPanels?.map((pluginPanelProps: any) => {
|
||||||
const pluginKey = panel.plugin || 'plugin';
|
const iconName: string = pluginPanelProps.icon || 'plugin';
|
||||||
return {
|
|
||||||
name: identifierString(`pluigin-${pluginKey}-${panel.name}`),
|
return PluginPanel({
|
||||||
label: panel.label || t`Plugin Panel`,
|
...pluginPanelProps,
|
||||||
icon: <InvenTreeIcon icon={panel.icon ?? 'plugin'} />,
|
name: identifierString(
|
||||||
content: (
|
`plugin-panel-${pluginPanelProps.plugin}-${pluginPanelProps.name}`
|
||||||
<PluginPanel
|
),
|
||||||
props={{
|
label: pluginPanelProps.label || t`Plugin Panel`,
|
||||||
...panel,
|
icon: <InvenTreeIcon icon={iconName as InvenTreeIconType} />,
|
||||||
id: id,
|
id: id,
|
||||||
model: model,
|
model: model,
|
||||||
instance: instance
|
instance: instance,
|
||||||
}}
|
pluginKey: pluginPanelProps.plugin || 'plugin'
|
||||||
/>
|
});
|
||||||
)
|
|
||||||
};
|
|
||||||
}) ?? []
|
}) ?? []
|
||||||
);
|
);
|
||||||
}, [data, id, model, instance]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
panels: panels
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user