From 8447f9a854708ddd0cc32a76be007205de0df58b Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 14 May 2023 15:06:34 +0200 Subject: [PATCH] Improved rendering of actions/arguments documentation. The frontend now calls `utils.rst_to_html` to render the docstrings as HTML instead of dumping them as raw text. Also, actions and arguments are now cached to improve performance. --- .../src/components/panels/Execute/Index.vue | 119 ++++++++++-------- .../src/components/panels/Execute/vars.scss | 4 +- .../http/webapp/src/style/themes/light.scss | 2 + platypush/plugins/inspect/__init__.py | 53 +++----- platypush/plugins/inspect/_model.py | 88 ++++--------- 5 files changed, 106 insertions(+), 160 deletions(-) diff --git a/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue b/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue index 2700ddf67..52a9a231f 100644 --- a/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue +++ b/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue @@ -30,8 +30,10 @@ Action documentation -
-
+
+ + +
-
-
+
+ + +
@@ -87,15 +91,17 @@ Attribute:
-
-
+
+ + +
+
-
-
-
-
-
+
+
+
+
@@ -163,6 +169,7 @@ export default { return { loading: false, running: false, + docLoading: false, structuredInput: true, actionChanged: false, selectedDoc: undefined, @@ -175,11 +182,11 @@ export default { response: undefined, error: undefined, - htmlDoc: false, rawRequest: undefined, actions: {}, plugins: {}, procedures: {}, + actionDocsCache: {}, action: { name: undefined, args: {}, @@ -195,15 +202,12 @@ export default { try { this.procedures = await this.request('inspect.get_procedures') - this.plugins = await this.request('inspect.get_all_plugins', {html_doc: false}) + this.plugins = await this.request('inspect.get_all_plugins') } finally { this.loading = false } for (const plugin of Object.values(this.plugins)) { - if (plugin.html_doc) - this.htmlDoc = true - for (const action of Object.values(plugin.actions)) { action.name = plugin.name + '.' + action.name action.supportsExtraArgs = !!action.has_kwargs @@ -213,20 +217,20 @@ export default { } const self = this - autocomplete(this.$refs.actionName, Object.keys(this.actions).sort(), (evt, value) => { + autocomplete(this.$refs.actionName, Object.keys(this.actions).sort(), (_, value) => { this.action.name = value self.updateAction() }) }, - updateAction() { + async updateAction() { if (!(this.action.name in this.actions)) this.selectedDoc = undefined if (!this.actionChanged || !(this.action.name in this.actions)) return - this.loading = true + this.docLoading = true try { this.action = { ...this.actions[this.action.name], @@ -241,32 +245,27 @@ export default { extraArgs: [], } } finally { - this.loading = false + this.docLoading = false } - this.selectedDoc = this.parseDoc(this.action.doc) + this.selectedDoc = + this.actionDocsCache[this.action.name]?.html || + await this.parseDoc(this.action.doc) + + if (!this.actionDocsCache[this.action.name]) + this.actionDocsCache[this.action.name] = {} + this.actionDocsCache[this.action.name].html = this.selectedDoc + this.actionChanged = false this.response = undefined this.error = undefined }, - parseDoc(docString) { - if (!docString?.length || this.htmlDoc) + async parseDoc(docString) { + if (!docString?.length) return docString - let lineNo = 0 - let trailingSpaces = 0 - - return docString.split('\n').reduce((doc, line) => { - if (++lineNo === 2) - trailingSpaces = line.match(/^(\s*)/)[1].length - - if (line.trim().startsWith('.. code-block')) - return doc - - doc += line.slice(trailingSpaces).replaceAll('``', '') + '\n' - return doc - }, '') + return await this.request('utils.rst_to_html', {text: docString}) }, updateProcedure(name, event) { @@ -308,11 +307,16 @@ export default { this.action.extraArgs.pop(i) }, - selectAttrDoc(name) { - this.response = undefined - this.error = undefined + async selectAttrDoc(name) { this.selectedAttr = name - this.selectedAttrDoc = this.parseDoc(this.action.args[name].doc) + this.selectedAttrDoc = + this.actionDocsCache[this.action.name]?.[name]?.html || + await this.parseDoc(this.action.args[name].doc) + + if (!this.actionDocsCache[this.action.name]) + this.actionDocsCache[this.action.name] = {} + + this.actionDocsCache[this.action.name][name] = {html: this.selectedAttrDoc} }, resetAttrDoc() { @@ -450,6 +454,7 @@ $params-tablet-width: 20em; } .action-form { + background: $default-bg-2; padding: 1em .5em; } @@ -544,12 +549,26 @@ $params-tablet-width: 20em; display: flex; margin-bottom: .5em; + label { + margin-left: 0.25em; + } + + .buttons { + display: flex; + flex-grow: 1; + justify-content: right; + } + .action-extra-param-del { border: 0; text-align: right; padding: 0 .5em; } + input[type=text] { + width: 100%; + } + .buttons { display: flex; align-items: center; @@ -569,11 +588,6 @@ $params-tablet-width: 20em; .doc-container, .output-container { margin-top: .5em; - .doc { - &.raw { - white-space: pre; - } - } } .output-container { @@ -590,7 +604,6 @@ $params-tablet-width: 20em; } .doc { - white-space: pre-line; width: 100%; overflow: auto; } @@ -613,11 +626,6 @@ $params-tablet-width: 20em; .attr-doc-container { .doc { padding: 1em !important; - - &.raw { - font-family: monospace; - font-size: .8em; - } } } @@ -747,11 +755,14 @@ $params-tablet-width: 20em; } .run-btn { - border-radius: 2em; - padding: .5em .75em; + background: none; + border-radius: .25em; + padding: .5em 1.5em; + box-shadow: $primary-btn-shadow; &:hover { - opacity: .8; + background: $hover-bg; + box-shadow: none; } } } diff --git a/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss b/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss index 76cd76d61..c120a666c 100644 --- a/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss +++ b/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss @@ -7,7 +7,7 @@ $response-bg: #edfff2; $response-border: 1px dashed #98ff98; $error-bg: #ffbcbc; $error-border: 1px dashed #ff5353; -$doc-bg: #e8feff; -$doc-border: 1px dashed #84f9ff; +$doc-bg: $background-color; +$doc-border: 1px dashed $border-color-2; $procedure-submit-btn-bg: #ebffeb; $section-title-bg: rgba(0, 0, 0, .04); diff --git a/platypush/backend/http/webapp/src/style/themes/light.scss b/platypush/backend/http/webapp/src/style/themes/light.scss index a148852de..3810ab257 100644 --- a/platypush/backend/http/webapp/src/style/themes/light.scss +++ b/platypush/backend/http/webapp/src/style/themes/light.scss @@ -56,6 +56,7 @@ $border-shadow-right: 2.5px 0 4px 0 $default-shadow-color; $border-shadow-bottom-right: 2.5px 2.5px 3px 0 $default-shadow-color; $header-shadow: 0px 1px 3px 1px #bbb !default; $group-shadow: 3px -2px 6px 1px #98b0a0; +$primary-btn-shadow: 1px 1px 0.5px 0.75px #32b64640 !default; //// Modals $modal-header-bg: #e0e0e0 !default; @@ -81,6 +82,7 @@ $default-hover-fg: #35b870 !default; $default-hover-fg-2: #38cf80 !default; $hover-fg: $default-hover-fg !default; $hover-bg: linear-gradient(90deg, rgba(190,246,218,1) 0%, rgba(229,251,240,1) 100%) !default; +$hover-bg-2: rgb(190,246,218) !default; $active-bg: #8fefb7 !default; /// Disabled diff --git a/platypush/plugins/inspect/__init__.py b/platypush/plugins/inspect/__init__.py index ccbb34dc0..abac0df1c 100644 --- a/platypush/plugins/inspect/__init__.py +++ b/platypush/plugins/inspect/__init__.py @@ -36,7 +36,6 @@ class InspectPlugin(Plugin): self._backends_lock = threading.RLock() self._events_lock = threading.RLock() self._responses_lock = threading.RLock() - self._html_doc = False def _get_modules(self, parent_class: type): for mf_file in scan_manifests(parent_class): @@ -57,9 +56,7 @@ class InspectPlugin(Plugin): for module in self._get_modules(Plugin): plugin_name = '.'.join(module.__name__.split('.')[2:]) plugin_class = get_plugin_class_by_name(plugin_name) - model = PluginModel( - plugin=plugin_class, prefix=prefix, html_doc=self._html_doc - ) + model = PluginModel(plugin=plugin_class, prefix=prefix) if model.name: self._plugins[model.name] = model @@ -70,9 +67,7 @@ class InspectPlugin(Plugin): for module in self._get_modules(Backend): for _, obj in inspect.getmembers(module): if inspect.isclass(obj) and issubclass(obj, Backend): - model = BackendModel( - backend=obj, prefix=prefix, html_doc=self._html_doc - ) + model = BackendModel(backend=obj, prefix=prefix) if model.name: self._backends[model.name] = model @@ -85,9 +80,7 @@ class InspectPlugin(Plugin): continue if inspect.isclass(obj) and issubclass(obj, Event) and obj != Event: - event = EventModel( - event=obj, html_doc=self._html_doc, prefix=prefix - ) + event = EventModel(event=obj, prefix=prefix) if event.package not in self._events: self._events[event.package] = {event.name: event} else: @@ -106,24 +99,19 @@ class InspectPlugin(Plugin): and issubclass(obj, Response) and obj != Response ): - response = ResponseModel( - response=obj, html_doc=self._html_doc, prefix=prefix - ) + response = ResponseModel(response=obj, prefix=prefix) if response.package not in self._responses: self._responses[response.package] = {response.name: response} else: self._responses[response.package][response.name] = response @action - def get_all_plugins(self, html_doc: Optional[bool] = None): + def get_all_plugins(self): """ - :param html_doc: If True then the docstring will be parsed into HTML (default: False) + Get information about all the available plugins. """ with self._plugins_lock: - if not self._plugins or ( - html_doc is not None and html_doc != self._html_doc - ): - self._html_doc = html_doc + if not self._plugins: self._init_plugins() return json.dumps( @@ -131,15 +119,12 @@ class InspectPlugin(Plugin): ) @action - def get_all_backends(self, html_doc: Optional[bool] = None): + def get_all_backends(self): """ - :param html_doc: If True then the docstring will be parsed into HTML (default: False) + Get information about all the available backends. """ with self._backends_lock: - if not self._backends or ( - html_doc is not None and html_doc != self._html_doc - ): - self._html_doc = html_doc + if not self._backends: self._init_backends() return json.dumps( @@ -147,15 +132,12 @@ class InspectPlugin(Plugin): ) @action - def get_all_events(self, html_doc: Optional[bool] = None): + def get_all_events(self): """ - :param html_doc: If True then the docstring will be parsed into HTML (default: False) + Get information about all the available events. """ with self._events_lock: - if not self._events or ( - html_doc is not None and html_doc != self._html_doc - ): - self._html_doc = html_doc + if not self._events: self._init_events() return json.dumps( @@ -166,15 +148,12 @@ class InspectPlugin(Plugin): ) @action - def get_all_responses(self, html_doc: Optional[bool] = None): + def get_all_responses(self): """ - :param html_doc: If True then the docstring will be parsed into HTML (default: False) + Get information about all the available responses. """ with self._responses_lock: - if not self._responses or ( - html_doc is not None and html_doc != self._html_doc - ): - self._html_doc = html_doc + if not self._responses: self._init_responses() return json.dumps( diff --git a/platypush/plugins/inspect/_model.py b/platypush/plugins/inspect/_model.py index b3e10d6b6..12824d432 100644 --- a/platypush/plugins/inspect/_model.py +++ b/platypush/plugins/inspect/_model.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod import inspect import json import re -from typing import Optional from platypush.utils import get_decorators @@ -14,16 +13,6 @@ class Model(ABC): def __repr__(self): return json.dumps(dict(self)) - @staticmethod - def to_html(doc): - try: - import docutils.core # type: ignore - except ImportError: - # docutils not found - return doc - - return docutils.core.publish_parts(doc, writer_name='html')['html_body'] - @abstractmethod def __iter__(self): raise NotImplementedError() @@ -40,42 +29,29 @@ class ProcedureEncoder(json.JSONEncoder): class BackendModel(Model): - def __init__(self, backend, prefix='', html_doc: Optional[bool] = False): + def __init__(self, backend, prefix=''): self.name = backend.__module__[len(prefix) :] - self.html_doc = html_doc - self.doc = ( - self.to_html(backend.__doc__) - if html_doc and backend.__doc__ - else backend.__doc__ - ) + self.doc = backend.__doc__ def __iter__(self): - for attr in ['name', 'doc', 'html_doc']: + for attr in ['name', 'doc']: yield attr, getattr(self, attr) class PluginModel(Model): - def __init__(self, plugin, prefix='', html_doc: Optional[bool] = False): - self.name = plugin.__module__[len(prefix) :] - self.html_doc = html_doc - self.doc = ( - self.to_html(plugin.__doc__) - if html_doc and plugin.__doc__ - else plugin.__doc__ - ) + def __init__(self, plugin, prefix=''): + self.name = re.sub(r'\._plugin$', '', plugin.__module__[len(prefix) :]) + self.doc = plugin.__doc__ self.actions = { - action_name: ActionModel( - getattr(plugin, action_name), html_doc=html_doc or False - ) + action_name: ActionModel(getattr(plugin, action_name)) for action_name in get_decorators(plugin, climb_class_hierarchy=True).get( 'action', [] ) } def __iter__(self): - for attr in ['name', 'actions', 'doc', 'html_doc']: + for attr in ['name', 'actions', 'doc']: if attr == 'actions': - # noinspection PyShadowingNames yield attr, { name: dict(action) for name, action in self.actions.items() }, @@ -84,40 +60,31 @@ class PluginModel(Model): class EventModel(Model): - def __init__(self, event, prefix='', html_doc: Optional[bool] = False): + def __init__(self, event, prefix=''): self.package = event.__module__[len(prefix) :] self.name = event.__name__ - self.html_doc = html_doc - self.doc = ( - self.to_html(event.__doc__) if html_doc and event.__doc__ else event.__doc__ - ) + self.doc = event.__doc__ def __iter__(self): - for attr in ['name', 'doc', 'html_doc']: + for attr in ['name', 'doc']: yield attr, getattr(self, attr) class ResponseModel(Model): - def __init__(self, response, prefix='', html_doc: Optional[bool] = False): + def __init__(self, response, prefix=''): self.package = response.__module__[len(prefix) :] self.name = response.__name__ - self.html_doc = html_doc - self.doc = ( - self.to_html(response.__doc__) - if html_doc and response.__doc__ - else response.__doc__ - ) + self.doc = response.__doc__ def __iter__(self): - for attr in ['name', 'doc', 'html_doc']: + for attr in ['name', 'doc']: yield attr, getattr(self, attr) class ActionModel(Model): - # noinspection PyShadowingNames - def __init__(self, action, html_doc: bool = False): + def __init__(self, action): self.name = action.__name__ - self.doc, argsdoc = self._parse_docstring(action.__doc__, html_doc=html_doc) + self.doc, argsdoc = self._parse_docstring(action.__doc__) self.args = {} self.has_kwargs = False @@ -134,7 +101,7 @@ class ActionModel(Model): } @classmethod - def _parse_docstring(cls, docstring: str, html_doc: bool = False): + def _parse_docstring(cls, docstring: str): new_docstring = '' params = {} cur_param = None @@ -147,11 +114,7 @@ class ActionModel(Model): m = re.match(r'^\s*:param ([^:]+):\s*(.*)', line) if m: if cur_param: - params[cur_param] = ( - cls.to_html(cur_param_docstring) - if html_doc - else cur_param_docstring - ) + params[cur_param] = cur_param_docstring cur_param = m.group(1) cur_param_docstring = m.group(2) @@ -160,11 +123,7 @@ class ActionModel(Model): else: if cur_param: if not line.strip(): - params[cur_param] = ( - cls.to_html(cur_param_docstring) - if html_doc - else cur_param_docstring - ) + params[cur_param] = cur_param_docstring cur_param = None cur_param_docstring = '' else: @@ -173,14 +132,9 @@ class ActionModel(Model): new_docstring += line.rstrip() + '\n' if cur_param: - params[cur_param] = ( - cls.to_html(cur_param_docstring) if html_doc else cur_param_docstring - ) + params[cur_param] = cur_param_docstring - return ( - new_docstring.strip() if not html_doc else cls.to_html(new_docstring), - params, - ) + return new_docstring.strip(), params def __iter__(self): for attr in ['name', 'args', 'doc', 'has_kwargs']: