Skip to content

User Interface Mixin

User Interface Mixin

The User Interface mixin class provides a set of methods to implement custom functionality for the InvenTree web interface.

Enable User Interface Mixin

To enable user interface plugins, the global setting ENABLE_PLUGINS_INTERFACE must be enabled, in the plugin settings.

Plugin Context

When rendering certain content in the user interface, the rendering functions are passed a context object which contains information about the current page being rendered. The type of the context object is defined in the PluginContext file:

Plugin Context
import {
  MantineColorScheme,
  MantineTheme,
  useMantineColorScheme,
  useMantineTheme
} from '@mantine/core';
import { AxiosInstance } from 'axios';
import { useMemo } from 'react';
import { NavigateFunction, useNavigate } from 'react-router-dom';

import { api } from '../../App';
import { useLocalState } from '../../states/LocalState';
import {
  SettingsStateProps,
  useGlobalSettingsState,
  useUserSettingsState
} from '../../states/SettingsState';
import { UserStateProps, useUserState } from '../../states/UserState';

/**
 * A set of properties which are passed to a plugin,
 * for rendering an element in the user interface.
 *
 * @param api - The Axios API instance (see ../states/ApiState.tsx)
 * @param user - The current user instance (see ../states/UserState.tsx)
 * @param userSettings - The current user settings (see ../states/SettingsState.tsx)
 * @param globalSettings - The global settings (see ../states/SettingsState.tsx)
 * @param navigate - The navigation function (see react-router-dom)
 * @param theme - The current Mantine theme
 * @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
 */
export type InvenTreeContext = {
  api: AxiosInstance;
  user: UserStateProps;
  userSettings: SettingsStateProps;
  globalSettings: SettingsStateProps;
  host: string;
  navigate: NavigateFunction;
  theme: MantineTheme;
  colorScheme: MantineColorScheme;
};

export const useInvenTreeContext = () => {
  const host = useLocalState((s) => s.host);
  const navigate = useNavigate();
  const user = useUserState();
  const { colorScheme } = useMantineColorScheme();
  const theme = useMantineTheme();
  const globalSettings = useGlobalSettingsState();
  const userSettings = useUserSettingsState();

  const contextData = useMemo<InvenTreeContext>(() => {
    return {
      user: user,
      host: host,
      api: api,
      navigate: navigate,
      globalSettings: globalSettings,
      userSettings: userSettings,
      theme: theme,
      colorScheme: colorScheme
    };
  }, [
    user,
    host,
    api,
    navigate,
    globalSettings,
    userSettings,
    theme,
    colorScheme
  ]);

  return contextData;
};

Custom Panels

Many of the pages in the InvenTree web interface are built using a series of "panels" which are displayed on the page. Custom panels can be added to these pages, by implementing the get_ui_panels method:

Return a list of custom panels to be injected into the UI.

Parameters:

Name Type Description Default
instance_type str

The type of object being viewed (e.g. 'part')

required
instance_id int

The ID of the object being viewed (e.g. 123)

required
request Request

HTTPRequest object (including user information)

required

Returns:

Name Type Description
list list[CustomPanel]

A list of custom panels to be injected into the UI

  • The returned list should contain a dict for each custom panel to be injected into the UI:
  • The following keys can be specified: { 'name': 'panel_name', # The name of the panel (required, must be unique) 'label': 'Panel Title', # The title of the panel (required, human readable) 'icon': 'icon-name', # Icon name (optional, must be a valid icon identifier) 'content': '

    Panel content

    ', # HTML content to be rendered in the panel (optional) 'context': {'key': 'value'}, # Context data to be passed to the front-end rendering function (optional) 'source': 'static/plugin/panel.js', # Path to a JavaScript file to be loaded (optional) }

  • Either 'source' or 'content' must be provided

Source code in src/backend/InvenTree/plugin/base/ui/mixins.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def get_ui_panels(
    self, instance_type: str, instance_id: int, request: Request, **kwargs
) -> list[CustomPanel]:
    """Return a list of custom panels to be injected into the UI.

    Args:
        instance_type: The type of object being viewed (e.g. 'part')
        instance_id: The ID of the object being viewed (e.g. 123)
        request: HTTPRequest object (including user information)

    Returns:
        list: A list of custom panels to be injected into the UI

    - The returned list should contain a dict for each custom panel to be injected into the UI:
    - The following keys can be specified:
    {
        'name': 'panel_name',  # The name of the panel (required, must be unique)
        'label': 'Panel Title',  # The title of the panel (required, human readable)
        'icon': 'icon-name',  # Icon name (optional, must be a valid icon identifier)
        'content': '<p>Panel content</p>',  # HTML content to be rendered in the panel (optional)
        'context': {'key': 'value'},  # Context data to be passed to the front-end rendering function (optional)
        'source': 'static/plugin/panel.js',  # Path to a JavaScript file to be loaded (optional)
    }

    - Either 'source' or 'content' must be provided

    """
    # Default implementation returns an empty list
    return []

The custom panels can display content which is generated either on the server side, or on the client side (see below).

Server Side Rendering

The panel content can be generated on the server side, by returning a 'content' attribute in the response. This 'content' attribute is expected to be raw HTML, and is rendered directly into the page. This is particularly useful for displaying static content.

Server-side rendering is simple to implement, and can make use of the powerful Django templating system.

Refer to the sample plugin for an example of how to implement server side rendering for custom panels.

Advantages:

  • Simple to implement
  • Can use Django templates to render content
  • Has access to the full InvenTree database, and content not available on the client side (via the API)

Disadvantages:

  • Content is rendered on the server side, and cannot be updated without a page refresh
  • Content is not interactive

Client Side Rendering

The panel content can also be generated on the client side, by returning a 'source' attribute in the response. This 'source' attribute is expected to be a URL which points to a JavaScript file which will be loaded by the client.

Refer to the sample plugin for an example of how to implement client side rendering for custom panels.

Panel Render Function

The JavaScript file must implement a renderPanel function, which is called by the client when the panel is rendered. This function is passed two parameters:

  • target: The HTML element which the panel content should be rendered into
  • context: A dictionary of context data which can be used to render the panel content

Example

export function renderPanel(target, context) {
    target.innerHTML = "<h1>Hello, world!</h1>";
}

Panel Visibility Function

The JavaScript file can also implement a isPanelHidden function, which is called by the client to determine if the panel is displayed. This function is passed a single parameter, context - which is the same as the context data passed to the renderPanel function.

The isPanelHidden function should return a boolean value, which determines if the panel is displayed or not, based on the context data.

If the isPanelHidden function is not implemented, the panel will be displayed by default.

Example

export function isPanelHidden(context) {
    // Only visible for active parts
    return context.model == 'part' && context.instance?.active;
}

Custom UI Functions

User interface plugins can also provide additional user interface functions. These functions can be provided via the get_ui_features method:

Return a list of custom features to be injected into the UI.

Parameters:

Name Type Description Default
feature_type FeatureType

The type of feature being requested

required
context dict

Additional context data provided by the UI

required
request Request

HTTPRequest object (including user information)

required

Returns:

Name Type Description
list list[UIFeature]

A list of custom UIFeature dicts to be injected into the UI

Source code in src/backend/InvenTree/plugin/base/ui/mixins.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def get_ui_features(
    self, feature_type: FeatureType, context: dict, request: Request
) -> list[UIFeature]:
    """Return a list of custom features to be injected into the UI.

    Arguments:
        feature_type: The type of feature being requested
        context: Additional context data provided by the UI
        request: HTTPRequest object (including user information)

    Returns:
        list: A list of custom UIFeature dicts to be injected into the UI
    """
    # Default implementation returns an empty list
    return []

Return a list of custom features to be injected into the UI.

Source code in src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def get_ui_features(self, feature_type, context, request):
    """Return a list of custom features to be injected into the UI."""
    if (
        feature_type == 'template_editor'
        and context.get('template_type') == 'labeltemplate'
    ):
        return [
            {
                'feature_type': 'template_editor',
                'options': {
                    'key': 'sample-template-editor',
                    'title': 'Sample Template Editor',
                    'icon': 'keywords',
                },
                'source': '/static/plugin/sample_template.js:getTemplateEditor',
            }
        ]

    if feature_type == 'template_preview':
        return [
            {
                'feature_type': 'template_preview',
                'options': {
                    'key': 'sample-template-preview',
                    'title': 'Sample Template Preview',
                    'icon': 'category',
                },
                'source': '/static/plugin/sample_template.js:getTemplatePreview',
            }
        ]

    return []

Currently the following functions can be extended:

Template editors

The template_editor feature type can be used to provide custom template editors.

Example:

sample_template.js
export function getTemplateEditor({ featureContext, pluginContext }) {
  const { ref } = featureContext;
  console.log("Template editor feature was called with", featureContext, pluginContext);
  const t = document.createElement("textarea");
  t.id = 'sample-template-editor-textarea';
  t.rows = 25;
  t.cols = 60;

  featureContext.registerHandlers({
    setCode: (code) => {
      t.value = code;
    },
    getCode: () => {
      return t.value;
    }
  });

  ref.innerHTML = "";
  ref.appendChild(t);
}

export function getTemplatePreview({ featureContext, pluginContext }) {
  const { ref } = featureContext;
  console.log("Template preview feature was called with", featureContext, pluginContext);

  featureContext.registerHandlers({
    updatePreview: (...args) => {
      console.log("updatePreview", args);
    }
  });

  ref.innerHTML = "<h1>Hello world</h1>";
}

Template previews

The template_preview feature type can be used to provide custom template previews. For an example see:

Example:

sample_template.js
export function getTemplateEditor({ featureContext, pluginContext }) {
  const { ref } = featureContext;
  console.log("Template editor feature was called with", featureContext, pluginContext);
  const t = document.createElement("textarea");
  t.id = 'sample-template-editor-textarea';
  t.rows = 25;
  t.cols = 60;

  featureContext.registerHandlers({
    setCode: (code) => {
      t.value = code;
    },
    getCode: () => {
      return t.value;
    }
  });

  ref.innerHTML = "";
  ref.appendChild(t);
}

export function getTemplatePreview({ featureContext, pluginContext }) {
  const { ref } = featureContext;
  console.log("Template preview feature was called with", featureContext, pluginContext);

  featureContext.registerHandlers({
    updatePreview: (...args) => {
      console.log("updatePreview", args);
    }
  });

  ref.innerHTML = "<h1>Hello world</h1>";
}

Sample Plugin

A sample plugin which implements custom user interface functionality is provided in the InvenTree source code:

A sample plugin which demonstrates user interface integrations.

Source code in src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlugin):
    """A sample plugin which demonstrates user interface integrations."""

    NAME = 'SampleUI'
    SLUG = 'sampleui'
    TITLE = 'Sample User Interface Plugin'
    DESCRIPTION = 'A sample plugin which demonstrates user interface integrations'
    VERSION = '1.1'

    SETTINGS = {
        'ENABLE_PART_PANELS': {
            'name': _('Enable Part Panels'),
            'description': _('Enable custom panels for Part views'),
            'default': True,
            'validator': bool,
        },
        'ENABLE_PURCHASE_ORDER_PANELS': {
            'name': _('Enable Purchase Order Panels'),
            'description': _('Enable custom panels for Purchase Order views'),
            'default': False,
            'validator': bool,
        },
        'ENABLE_BROKEN_PANELS': {
            'name': _('Enable Broken Panels'),
            'description': _('Enable broken panels for testing'),
            'default': True,
            'validator': bool,
        },
        'ENABLE_DYNAMIC_PANEL': {
            'name': _('Enable Dynamic Panel'),
            'description': _('Enable dynamic panels for testing'),
            'default': True,
            'validator': bool,
        },
    }

    def get_ui_panels(self, instance_type: str, instance_id: int, request, **kwargs):
        """Return a list of custom panels to be injected into the UI."""
        panels = []

        # First, add a custom panel which will appear on every type of page
        # This panel will contain a simple message

        content = render_text(
            """
            This is a <i>sample panel</i> which appears on every page.
            It renders a simple string of <b>HTML</b> content.

            <br>
            <h5>Instance Details:</h5>
            <ul>
            <li>Instance Type: {{ instance_type }}</li>
            <li>Instance ID: {{ instance_id }}</li>
            </ul>
            """,
            context={'instance_type': instance_type, 'instance_id': instance_id},
        )

        panels.append({
            'name': 'sample_panel',
            'label': 'Sample Panel',
            'content': content,
        })

        # A broken panel which tries to load a non-existent JS file
        if self.get_setting('ENABLE_BROKEN_PANElS'):
            panels.append({
                'name': 'broken_panel',
                'label': 'Broken Panel',
                'source': '/this/does/not/exist.js',
            })

        # A dynamic panel which will be injected into the UI (loaded from external file)
        # Note that we additionally provide some "context" data to the front-end render function
        if self.get_setting('ENABLE_DYNAMIC_PANEL'):
            panels.append({
                'name': 'dynamic_panel',
                'label': 'Dynamic Part Panel',
                'source': '/static/plugin/sample_panel.js',
                'context': {
                    'version': INVENTREE_SW_VERSION,
                    'plugin_version': self.VERSION,
                    'random': random.randint(1, 100),
                    'time': time.time(),
                },
                'icon': 'part',
            })

        # Next, add a custom panel which will appear on the 'part' page
        # Note that this content is rendered from a template file,
        # using the django templating system
        if self.get_setting('ENABLE_PART_PANELS') and instance_type == 'part':
            try:
                part = Part.objects.get(pk=instance_id)
            except (Part.DoesNotExist, ValueError):
                part = None

            # Note: This panel will *only* be available if the part is active
            if part and part.active:
                content = render_template(
                    self, 'uidemo/custom_part_panel.html', context={'part': part}
                )

                panels.append({
                    'name': 'part_panel',
                    'label': 'Part Panel',
                    'content': content,
                })

        # Next, add a custom panel which will appear on the 'purchaseorder' page
        if (
            self.get_setting('ENABLE_PURCHASE_ORDER_PANELS')
            and instance_type == 'purchaseorder'
        ):
            panels.append({
                'name': 'purchase_order_panel',
                'label': 'Purchase Order Panel',
                'content': 'This is a custom panel which appears on the <b>Purchase Order</b> view page.',
            })

        return panels

    def get_ui_features(self, feature_type, context, request):
        """Return a list of custom features to be injected into the UI."""
        if (
            feature_type == 'template_editor'
            and context.get('template_type') == 'labeltemplate'
        ):
            return [
                {
                    'feature_type': 'template_editor',
                    'options': {
                        'key': 'sample-template-editor',
                        'title': 'Sample Template Editor',
                        'icon': 'keywords',
                    },
                    'source': '/static/plugin/sample_template.js:getTemplateEditor',
                }
            ]

        if feature_type == 'template_preview':
            return [
                {
                    'feature_type': 'template_preview',
                    'options': {
                        'key': 'sample-template-preview',
                        'title': 'Sample Template Preview',
                        'icon': 'category',
                    },
                    'source': '/static/plugin/sample_template.js:getTemplatePreview',
                }
            ]

        return []