diff --git a/src/frontend/src/components/plugins/PluginPanel.tsx b/src/frontend/src/components/plugins/PluginPanel.tsx
new file mode 100644
index 0000000000..aef7959ed3
--- /dev/null
+++ b/src/frontend/src/components/plugins/PluginPanel.tsx
@@ -0,0 +1,93 @@
+import { t } from '@lingui/macro';
+import { Alert, Text } from '@mantine/core';
+import { AxiosInstance } from 'axios';
+import { useEffect, useRef } from 'react';
+
+import { api } from '../../App';
+import { ModelType } from '../../enums/ModelType';
+import { PanelType } from '../nav/Panel';
+
+interface PluginPanelProps extends PanelType {
+ src?: string;
+ params?: any;
+ targetModel?: ModelType | string;
+ targetId?: string | number | null;
+}
+
+/*
+ * Definition of what we pass into a plugin panel
+ */
+interface PluginPanelParameters {
+ target: HTMLDivElement;
+ props: PluginPanelProps;
+ targetModel?: ModelType | string;
+ targetId?: number;
+ api: AxiosInstance;
+}
+
+// Placeholder content for a panel with no content
+function PanelNoContent() {
+ return (
+
+ {t`No content provided for this plugin`}
+
+ );
+}
+
+/**
+ * TODO: Provide more context information to the plugin renderer:
+ *
+ * - api instance
+ * - custom context data from server
+ */
+
+/**
+ * A custom panel which can be used to display plugin content.
+ *
+ * - Content is loaded dynamically (via the API) when a page is first loaded
+ * - Content can be provided from an external javascript module, or with raw HTML
+ *
+ * If content is provided from an external source, it is expected to define a function `render_panel` which will render the content.
+ * const render_panel = (element: HTMLElement, params: any) => {...}
+ *
+ * Where:
+ * - `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
+ */
+export default function PluginPanel({ props }: { props: PluginPanelProps }) {
+ const ref = useRef();
+
+ const loadExternalSource = async () => {
+ // Load content from external source
+ const src = await import(/* @vite-ignore */ props.src ?? '');
+
+ // We expect the external source to define a function which will render the content
+ if (src && src.render_panel && typeof src.render_panel === 'function') {
+ src.render_panel({
+ target: ref.current,
+ props: props,
+ api: api,
+ targetModel: props.targetModel,
+ targetId: props.targetId
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (props.src) {
+ // Load content from external source
+ loadExternalSource();
+ } else if (props.content) {
+ // If content is provided directly, render it into the panel
+ // ref.current.innerHTML = props.content;
+ } else {
+ // Something... went wrong?
+ }
+ }, [props]);
+
+ if (!props.content && !props.src) {
+ return ;
+ }
+
+ return {props.content}
;
+}
diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx
index 52382bc995..b37d311e74 100644
--- a/src/frontend/src/hooks/UsePluginPanels.tsx
+++ b/src/frontend/src/hooks/UsePluginPanels.tsx
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { api } from '../App';
import { PanelType } from '../components/nav/Panel';
+import PluginPanel from '../components/plugins/PluginPanel';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { identifierString } from '../functions/conversion';
@@ -16,15 +17,6 @@ export type PluginPanelState = {
panels: PanelType[];
};
-// Placeholder content for a panel with no content
-function PanelNoContent() {
- return (
-
- {t`No content provided for this plugin`}
-
- );
-}
-
export function usePluginPanels({
targetModel,
targetId
@@ -39,6 +31,7 @@ export function usePluginPanels({
[globalSettings]
);
+ // API query to fetch information on available plugin panels
const { isFetching, data } = useQuery({
enabled: pluginPanelsEnabled && !!targetModel,
queryKey: [targetModel, targetId],
@@ -70,7 +63,7 @@ export function usePluginPanels({
name: identifierString(`${pluginKey}-${panel.name}`),
label: panel.label || t`Plugin Panel`,
icon: ,
- content: panel.content ||
+ content:
};
}) ?? []
);