Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PUI] Dashboard refactor #8278

Open
wants to merge 89 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
40c21c5
Refactor plugin components into <RemoteComponent />
SchrodingersGat Oct 12, 2024
1f29019
Clean up footer
SchrodingersGat Oct 12, 2024
7cbb699
Allow BuildOrder list to be sorted by 'outstanding'
SchrodingersGat Oct 12, 2024
f12778b
Fix model name
SchrodingersGat Oct 12, 2024
c457697
Update BuildOrderTable filter
SchrodingersGat Oct 12, 2024
4adac19
Add StockItemTable column
SchrodingersGat Oct 12, 2024
55f11be
Working towards new dashboard
SchrodingersGat Oct 12, 2024
126bebe
Cleanup unused imports
SchrodingersGat Oct 12, 2024
103fdf8
Updates: Now rendering some custom widgets
SchrodingersGat Oct 12, 2024
f910dab
Define icons for model types
SchrodingersGat Oct 12, 2024
7e11789
Add icon
SchrodingersGat Oct 12, 2024
e77064c
Cleanup / refactor / delete
SchrodingersGat Oct 12, 2024
326eb07
Follow link for query count widgets
SchrodingersGat Oct 12, 2024
c863eab
Add some more widgets to the library
SchrodingersGat Oct 12, 2024
bc136e0
Remove old dashboard link in header
SchrodingersGat Oct 12, 2024
0706269
Remove feedback widget
SchrodingersGat Oct 12, 2024
2112b26
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 12, 2024
5694445
Bump API version
SchrodingersGat Oct 12, 2024
3c6e11d
Remove test widget
SchrodingersGat Oct 12, 2024
7e2d140
Rename "Home" -> "Dashboard"
SchrodingersGat Oct 12, 2024
d93c51a
Add some more widgets
SchrodingersGat Oct 12, 2024
4a16b56
Pass 'editable' property through to widgets
SchrodingersGat Oct 12, 2024
3230169
Cleanup
SchrodingersGat Oct 12, 2024
533f699
Add drawer for selecting new widgets
SchrodingersGat Oct 13, 2024
b2872d2
Allow different layouts per user on the same machine
SchrodingersGat Oct 13, 2024
f964beb
Fixes
SchrodingersGat Oct 13, 2024
af140bd
Add ability to *remove* widgets
SchrodingersGat Oct 13, 2024
e48f505
Add helpful button
SchrodingersGat Oct 13, 2024
ab19ef4
Add a keyboard shortcut
SchrodingersGat Oct 13, 2024
4237daa
Refactoring
SchrodingersGat Oct 13, 2024
ce44952
Add backend code for serving custom dashboard items
SchrodingersGat Oct 13, 2024
3da2799
Load dashboard items from plugins
SchrodingersGat Oct 13, 2024
b5eeb53
Tweak for dashboard item API query
SchrodingersGat Oct 13, 2024
a335800
Add message if no dashboard widgets are displayed
SchrodingersGat Oct 13, 2024
d92cf74
Refactoring main navigation menu
SchrodingersGat Oct 13, 2024
9b3216a
Remove playground
SchrodingersGat Oct 13, 2024
88d13c2
Add backend field for storing dashboard layout
SchrodingersGat Oct 13, 2024
148158f
Add extra type definitions for UseInstance
SchrodingersGat Oct 13, 2024
2ad1697
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 14, 2024
7b06cc0
Manual labels for builtin dashboard items
SchrodingersGat Oct 14, 2024
729105b
Shorten labels for more plugins
SchrodingersGat Oct 15, 2024
43f7d72
Adjust DashboardMenu
SchrodingersGat Oct 15, 2024
6b1583f
Reduce stored data
SchrodingersGat Oct 15, 2024
f900993
Add widget filter by text
SchrodingersGat Oct 15, 2024
1110dad
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 15, 2024
e3082de
Remove back-end settings
SchrodingersGat Oct 15, 2024
0fd7371
Update playwright tests for dashboard
SchrodingersGat Oct 15, 2024
c13c754
Updated tests
SchrodingersGat Oct 15, 2024
49878f7
Refactor backend API for fetching plugin features
SchrodingersGat Oct 15, 2024
0619fa5
Further fixes for back-end code
SchrodingersGat Oct 15, 2024
e5bb55a
More back-end fixes
SchrodingersGat Oct 15, 2024
afaf4a8
Refactor frontend:
SchrodingersGat Oct 15, 2024
fe5b51d
Further backend fixes
SchrodingersGat Oct 15, 2024
98d8c1d
Yet more backend fixes
SchrodingersGat Oct 15, 2024
397a1fa
Fix for custom plugin settings rendering
SchrodingersGat Oct 15, 2024
9b08dff
Enable plugin panels for part index and stock index pages
SchrodingersGat Oct 15, 2024
9fb58c0
Cleanup
SchrodingersGat Oct 15, 2024
a02f855
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 15, 2024
717e607
Fix nav menu
SchrodingersGat Oct 15, 2024
88c8223
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 16, 2024
47182da
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 17, 2024
fd8d7af
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 19, 2024
bb14cfa
Update typing
SchrodingersGat Oct 19, 2024
92bb647
Helper func to return all plugin settings as a dict
SchrodingersGat Oct 19, 2024
00eabad
Merge branch 'dashboard-refactor' of github.com:SchrodingersGat/Inven…
SchrodingersGat Oct 19, 2024
b841296
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 20, 2024
24bbedd
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 20, 2024
6668724
Merge branch 'dashboard-refactor' of github.com:SchrodingersGat/Inven…
SchrodingersGat Oct 23, 2024
6c3c18b
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 23, 2024
9e502d0
Update API version date
SchrodingersGat Oct 23, 2024
d7fe05c
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 23, 2024
10adb64
Merge remote-tracking branch 'origin/master' into dashboard-refactor
SchrodingersGat Oct 24, 2024
156b408
Fix for UseInstancea
SchrodingersGat Oct 24, 2024
f30cf34
typing fix
SchrodingersGat Oct 24, 2024
087a519
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 25, 2024
49ce9df
Tweak layout callbacks
SchrodingersGat Oct 25, 2024
cf182c9
Pass query parameters through to navigation functions
SchrodingersGat Oct 25, 2024
bf68968
Improve custom query display
SchrodingersGat Oct 25, 2024
1565522
Add "news" widget
SchrodingersGat Oct 26, 2024
4822931
Ensure links are prepended with base URL on receipt
SchrodingersGat Oct 26, 2024
27a19ea
Update NewsWidget
SchrodingersGat Oct 26, 2024
d3fd087
Bug fix
SchrodingersGat Oct 26, 2024
0ac7138
Refactor template editor tests
SchrodingersGat Oct 26, 2024
6c16fa1
Refactor unit testing for test_ui_panels
SchrodingersGat Oct 26, 2024
95d6bcb
Unit test for dashboard item API endpoint
SchrodingersGat Oct 27, 2024
c939ecc
Merge branch 'master' into dashboard-refactor
SchrodingersGat Oct 27, 2024
a0f01cc
Update comment
SchrodingersGat Oct 27, 2024
6deaabe
Adjust playwright tests
SchrodingersGat Oct 27, 2024
b23abd3
More playwright fixes
SchrodingersGat Oct 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 272
INVENTREE_API_VERSION = 273

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """

v273 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8278
- Allow build order list to be filtered by "outstanding" (alias for "active")

v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
- Adjustments to BuildLine API serializers

Expand Down
3 changes: 3 additions & 0 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class Meta:

active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')

# 'outstanding' is an alias for 'active' here
outstanding = rest_filters.BooleanFilter(label='Build is outstanding', method='filter_active')

def filter_active(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are active."""
if str2bool(value):
Expand Down
4 changes: 4 additions & 0 deletions src/backend/InvenTree/build/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ def test_create_delete_output(self):
# Now, let's delete each build output individually via the API
outputs = bo.build_outputs.all()

# Assert that each output is currently in production
for output in outputs:
self.assertTrue(output.is_building)

delete_url = reverse('api-build-output-delete', kwargs={'pk': 1})

response = self.post(
Expand Down
4 changes: 3 additions & 1 deletion src/backend/InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,9 @@ def run_validator(self, validator):
except ValidationError as e:
raise e
except Exception:
raise ValidationError({'value': _('Invalid value')})
raise ValidationError({
'value': _('Value does not pass validation checks')
})

def validate_unique(self, exclude=None):
"""Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison.
Expand Down
29 changes: 29 additions & 0 deletions src/backend/InvenTree/plugin/base/integration/SettingsMixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,32 @@ def check_settings(self):
return PluginSetting.check_all_settings(
settings_definition=self.settings, plugin=self.plugin_config()
)

def get_settings_dict(self) -> dict:
"""Return a dictionary of all settings for this plugin.

- For each setting, return <key>: <value> pair.
- If the setting is not defined, return the default value (if defined).

Returns:
dict: Dictionary of all settings for this plugin
"""
from plugin.models import PluginSetting

keys = self.settings.keys()

settings = PluginSetting.objects.filter(
plugin=self.plugin_config(), key__in=keys
)

settings_dict = {}

for setting in settings:
settings_dict[setting.key] = setting.value

# Add any missing settings
for key in keys:
if key not in settings_dict:
settings_dict[key] = self.settings[key].get('default')

return settings_dict
84 changes: 34 additions & 50 deletions src/backend/InvenTree/plugin/base/ui/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,6 @@
from plugin import registry


class PluginPanelList(APIView):
"""API endpoint for listing all available plugin panels."""

permission_classes = [IsAuthenticated]
serializer_class = UIPluginSerializers.PluginPanelSerializer

@extend_schema(
responses={200: UIPluginSerializers.PluginPanelSerializer(many=True)}
)
def get(self, request):
"""Show available plugin panels."""
target_model = request.query_params.get('target_model', None)
target_id = request.query_params.get('target_id', None)

panels = []

if get_global_setting('ENABLE_PLUGINS_INTERFACE'):
# Extract all plugins from the registry which provide custom panels
for _plugin in registry.with_mixin('ui', active=True):
try:
# Allow plugins to fill this data out
plugin_panels = _plugin.get_ui_panels(
target_model, target_id, request
)

if plugin_panels and type(plugin_panels) is list:
for panel in plugin_panels:
panel['plugin'] = _plugin.slug

# TODO: Validate each panel before inserting
panels.append(panel)
except Exception:
# Custom panels could not load
# Log the error and continue
log_error(f'{_plugin.slug}.get_ui_panels')

return Response(
UIPluginSerializers.PluginPanelSerializer(panels, many=True).data
)


class PluginUIFeatureList(APIView):
"""API endpoint for listing all available plugin ui features."""

Expand All @@ -71,24 +30,49 @@ def get(self, request, feature):
# Extract all plugins from the registry which provide custom ui features
for _plugin in registry.with_mixin('ui', active=True):
# Allow plugins to fill this data out
plugin_features = _plugin.get_ui_features(
feature, request.query_params, request
)

try:
plugin_features = _plugin.get_ui_features(
feature, request.query_params, request
)
except Exception:
# Custom features could not load for this plugin
# Log the error and continue
log_error(f'{_plugin.slug}.get_ui_features')
continue

if plugin_features and type(plugin_features) is list:
for _feature in plugin_features:
features.append(_feature)
try:
# Ensure that the required fields are present
_feature['plugin_name'] = _plugin.slug
_feature['feature_type'] = str(feature)

# Ensure base fields are strings
for field in ['key', 'title', 'description', 'source']:
if field in _feature:
_feature[field] = str(_feature[field])

return Response(
UIPluginSerializers.PluginUIFeatureSerializer(features, many=True).data
)
# Add the feature to the list (serialize)
features.append(
UIPluginSerializers.PluginUIFeatureSerializer(
_feature, many=False
).data
)

except Exception:
# Custom features could not load
# Log the error and continue
log_error(f'{_plugin.slug}.get_ui_features')
continue

return Response(features)


ui_plugins_api_urls = [
path('panels/', PluginPanelList.as_view(), name='api-plugin-panel-list'),
path(
'features/<str:feature>/',
PluginUIFeatureList.as_view(),
name='api-plugin-ui-feature-list',
),
)
]
137 changes: 94 additions & 43 deletions src/backend/InvenTree/plugin/base/ui/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,57 @@
logger = logging.getLogger('inventree')


class CustomPanel(TypedDict):
"""Type definition for a custom panel.

Attributes:
name: The name of the panel (required, used as a DOM identifier).
label: The label of the panel (required, human readable).
icon: The icon of the panel (optional, must be a valid icon identifier).
content: The content of the panel (optional, raw HTML).
context: Optional context data (dict / JSON) which will be passed to the front-end rendering function
source: The source of the panel (optional, path to a JavaScript file).
"""

name: str
label: str
icon: str
content: str
context: dict
source: str


FeatureType = Literal['template_editor', 'template_preview']
# List of supported feature types
FeatureType = Literal[
'dashboard', # Custom dashboard items
'panel', # Custom panels
'template_editor', # Custom template editor
'template_preview', # Custom template preview
]


class UIFeature(TypedDict):
"""Base type definition for a ui feature.

Attributes:
key: The key of the feature (required, must be a unique identifier)
title: The title of the feature (required, human readable)
description: The long-form description of the feature (optional, human readable)
feature_type: The feature type (required, see documentation for all available types)
options: Feature options (required, see documentation for all available options for each type)
source: The source of the feature (required, path to a JavaScript file).
"""

key: str
title: str
description: str
feature_type: FeatureType
options: dict
source: str


class CustomPanelOptions(TypedDict):
"""Options type definition for a custom panel.

Attributes:
icon: The icon of the panel (optional, must be a valid icon identifier).
"""

icon: str


class CustomDashboardItemOptions(TypedDict):
"""Options type definition for a custom dashboard item.

Attributes:
width: The minimum width of the dashboard item (integer, defaults to 2)
height: The minimum height of the dashboard item (integer, defaults to 2)
"""

width: int
height: int


class UserInterfaceMixin:
"""Plugin mixin class which handles injection of custom elements into the front-end interface.

Expand All @@ -65,48 +79,85 @@ def __init__(self):
super().__init__()
self.add_mixin('ui', True, __class__) # type: ignore

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 (query parameters)
request: HTTPRequest object (including user information)

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

"""
feature_map = {
'dashboard': self.get_ui_dashboard_items,
'panel': self.get_ui_panels,
'template_editor': self.get_ui_template_editors,
'template_preview': self.get_ui_template_previews,
}

if feature_type in feature_map:
return feature_map[feature_type](request, context=context)
else:
logger.warning(f'Invalid feature type: {feature_type}')
return []

def get_ui_panels(
self, instance_type: str, instance_id: int, request: Request, **kwargs
) -> list[CustomPanel]:
self, request: Request, context: dict, **kwargs
) -> list[UIFeature]:
"""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
"""
# Default implementation returns an empty list
return []

- 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)
}
def get_ui_dashboard_items(
self, request: Request, context: dict, **kwargs
) -> list[UIFeature]:
"""Return a list of custom dashboard items to be injected into the UI.

- Either 'source' or 'content' must be provided
Args:
request: HTTPRequest object (including user information)

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

def get_ui_features(
self, feature_type: FeatureType, context: dict, request: Request
def get_ui_template_editors(
self, request: Request, context: dict
) -> list[UIFeature]:
"""Return a list of custom features to be injected into the UI.
"""Return a list of custom template editors to be injected into the UI.

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

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

def get_ui_template_previews(
self, request: Request, context: dict
) -> list[UIFeature]:
"""Return a list of custom template previews to be injected into the UI.

Args:
request: HTTPRequest object (including user information)

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