#401 Allow using other tags than input in the widget config
Merged 6 years ago by abompard. Opened 6 years ago by abompard.
abompard/fedora-hubs feature/widget-params  into  develop

file modified
+5
@@ -17,6 +17,11 @@ 

  .. automodule:: hubs.widgets.base

     :private-members:

  

+ Widget parameters

+ -----------------

+ 

+ .. automodule:: hubs.widgets.parameters

+ 

  Widget parameter validators

  ---------------------------

  

@@ -9,6 +9,16 @@ 

  import { makeLoadable } from '../../core/utils';

  

  

+ const AsyncComponent = makeLoadable(

+   (props) => import(

+     /* webpackChunkName: "[request]" */

+     `../../widgets/${props.widget.name}/Config`

+   ),

+   "Loading widget configuration...",

+   "Sorry, there was a problem loading the widget configuration."

+ );

+ 

+ 

  class AddWidgetDialog extends React.Component {

  

    constructor(props) {
@@ -117,11 +127,6 @@ 

        // Second step: configure the widget

        title = "Adding widget " + this.state.selectedWidget.label;

        if (this.state.selectedWidget.isReact) {

-         const AsyncComponent = makeLoadable(

-           () => import(`../../widgets/${this.state.selectedWidget.name}/Config`),

-           "Loading widget configuration...",

-           "Sorry, there was a problem loading the widget configuration."

-         );

          contents = (

            <AsyncComponent

              widget={this.state.selectedWidget}

@@ -76,7 +76,7 @@ 

          editMode={this.props.editMode}

          >

          { this.state.isLoading &&

-           <div className="text-muted">Loading...</div>

+           <div className="p-2 text-muted">Loading...</div>

            ||

            <div className="p-2" dangerouslySetInnerHTML={{__html: this.state.contents}} />

          }

@@ -14,9 +14,11 @@ 

        return (

          <fieldset className="form-group row" key={field.name}>

            <strong>{field.label}</strong>

-           <input

+           <field.renderTag

              className="form-control"

-             type="text"

+             type={

+               field.renderTag === "input" ? field.renderType : null

+             }

              value={this.props.values[field.name] || ""}

              name={field.name}

              onChange={(e) => {

@@ -5,6 +5,16 @@ 

  import SimpleWidget from '../components/SimpleWidget';

  

  

+ const AsyncComponent = makeLoadable(

+   (props) => import(

+     /* webpackChunkName: "[request]" */

+     `../widgets/${props.widget.component}/Widget`

+   ),

+   "Loading widget...",

+   "Sorry, there was a problem loading the widget."

+ );

+ 

+ 

  class Widget extends React.PureComponent {

  

    propTypes: {
@@ -20,11 +30,6 @@ 

    render() {

      let widgetComponent;

      if (!this.props.editMode && this.props.widget.isReact) {

-       const AsyncComponent = makeLoadable(

-         () => import(`../widgets/${this.props.widget.component}/Widget`),

-         "Loading widget...",

-         "Sorry, there was a problem loading the widget."

-       );

        widgetComponent = (

          <AsyncComponent {...this.props} />

        );

@@ -11,6 +11,16 @@ 

  import Modal from '../components/Modal';

  

  

+ const AsyncComponent = makeLoadable(

+   (props) => import(

+     /* webpackChunkName: "[request]" */

+     `../widgets/${props.widget.name}/Config`

+   ),

+   "Loading widget configuration...",

+   "Sorry, there was a problem loading the widget configuration."

+ );

+ 

+ 

  class WidgetConfigDialog extends React.Component {

  

    propTypes: {
@@ -64,14 +74,8 @@ 

      if (!this.props.widget) {

        return null;

      }

-     const widgetName = this.props.widget.name;

      let contents;

      if (this.props.widget.isReact) {

-       const AsyncComponent = makeLoadable(

-         () => import(`../widgets/${widgetName}/Config`),

-         "Loading widget configuration...",

-         "Sorry, there was a problem loading the widget configuration."

-       );

        contents = (

          <AsyncComponent

            widget={this.props.widget}

@@ -4,12 +4,12 @@ 

  } from '../core/utils';

  

  

- let Hub = makeLoadable(

+ const Hub = makeLoadable(

    () => import(/* webpackChunkName: "page-hub" */ '../components/HubPage'),

    "Loading...",

    "Sorry, there was a problem loading the page."

  );

- let Streams = makeLoadable(

+ const Streams = makeLoadable(

    () => import(/* webpackChunkName: "page-streams" */ '../components/StreamsPage'),

    "Loading...",

    "Sorry, there was a problem loading the page."

@@ -1,6 +1,8 @@ 

  // Read the public path from the backend

  // https://webpack.js.org/guides/public-path/

- __webpack_public_path__ = window.resourceBaseUrl;

+ if(window.resourceBaseUrl) {

+   __webpack_public_path__ = window.resourceBaseUrl;

+ }

  

  import React from 'react';

  import ReactDOM from 'react-dom';

@@ -159,7 +159,10 @@ 

                  {'default': 'I am a Fedora user, and this is my about',

                   'help': 'Text about a user.',

                   'label': 'Text',

-                  'name': 'text'},

+                  'name': 'text',

+                  'renderTag': 'input',

+                  'renderType': 'text',

+                  },

              ],

              'position': 'right',

              'selfUrl': '/api/hubs/ralph/widgets/{}/'.format(widget.idx),

@@ -128,11 +128,15 @@ 

                   'help': 'A comma-separated list of hubs to monitor.',

                   'label': 'Hubs',

                   'name': 'hubs',

+                  'renderTag': 'input',

+                  'renderType': 'text',

                   },

                  {'default': 4,

                   'help': 'The number of requests per page to display.',

                   'label': 'Requests per page',

                   'name': 'per_page',

+                  'renderTag': 'input',

+                  'renderType': 'text',

                   },

              ],

              'position': 'right',

file modified
+4 -48
@@ -9,50 +9,12 @@ 

  from importlib import import_module

  from .caching import CachedFunction

  from .view import WidgetView

+ from .parameters import WidgetParameter

  

  

  log = logging.getLogger(__name__)

  

  

- class WidgetParameter(object):

-     """

-     Configuration option for a widget.

- 

-     A widget can be configured differently for each instance of this widget in

-     a hub.  The list of configuration options is described by the list of

-     :py:class:`WidgetParameter` objects returned by the widget's

-     :py:meth:`~Widget.get_parameters` method.

- 

-     The value of the parameter is stored in the database as the value returned

-     by the validator's call.  It can thus be a string, an integer, a list, a

-     dict, or any JSON-serializable value.

- 

-     Attributes:

-         name (str): The name of the parameter.

-         label (str): A humanized name of the parameter, which will be shown in

-             the UI.

-         default: The default value if this parameter is not set.

-         validator (callable): A validator function that will be used to convert

-             the parameter value to a JSON-serializable value, raising an

-             exception if it is invalid.

-         help (str): A help text that will be shown in the UI.

-         secret (bool): If True, this parameter will not be exposed to users

-             viewing the widget, it will only be available in the widget

-             configuration.

-     """

- 

-     _attrs = ('name', 'label', 'default', 'validator', 'help', 'secret')

- 

-     def __init__(self, **kwargs):

-         for name in self._attrs:

-             setattr(self, name, kwargs.pop(name, None))

-         for name in kwargs:

-             raise TypeError("Invalid attribute: %s" % name)

-         # Set default validator

-         if self.validator is None:

-             self.validator = lambda x: x

- 

- 

  class Widget(object):

      """

      The main widget class, you must subclass it to create a widget.
@@ -254,16 +216,10 @@ 

              hiddenIfEmpty=self.hidden_if_empty,

              cssClass=self.display_css,

              isReact=self.is_react,

-             params=[],

+             params=[

+                 param.to_dict() for param in self.get_parameters()

+             ],

          )

-         for param in self.get_parameters():

-             param_data = dict(

-                 name=param.name,

-                 label=param.label,

-                 default=param.default,

-                 help=param.help,

-                 )

-             props["params"].append(param_data)

          if instance is not None:

              props.update({

                  "idx": instance.idx,

@@ -0,0 +1,65 @@ 

+ from __future__ import unicode_literals, absolute_import

+ 

+ from .validators import Noop

+ 

+ 

+ class WidgetParameter(object):

+     """

+     Configuration option for a widget.

+ 

+     A widget can be configured differently for each instance of this widget in

+     a hub.  The list of configuration options is described by the list of

+     :py:class:`WidgetParameter` objects returned by the widget's

+     :py:meth:`~Widget.get_parameters` method.

+ 

+     The value of the parameter is stored in the database as the value returned

+     by the validator's call.  It can thus be a string, an integer, a list, a

+     dict, or any JSON-serializable value.

+ 

+     Attributes:

+         name (str): The name of the parameter.

+         label (str): A humanized name of the parameter, which will be shown in

+             the UI.

+         default: The default value if this parameter is not set.

+         validator (callable): A validator function that will be used to convert

+             the parameter value to a JSON-serializable value, raising an

+             exception if it is invalid.

+         help (str): A help text that will be shown in the UI.

+         secret (bool): If True, this parameter will not be exposed to users

+             viewing the widget, it will only be available in the widget

+             configuration.

+         render_tag (str): The HTML tag to use when rendering.

+         render_type (str): The HTML type attribute to use when rendering if the

+             tag is ``input``.

+     """

+ 

+     _defaults = {

+         "label": None,

+         "default": None,

+         "help": None,

+         "validator": Noop,

+         "secret": False,

+         "render_tag": "input",

+         "render_type": "text",

+     }

+ 

+     def __init__(self, name, **kwargs):

+         self.name = name

+         for attr, default in self._defaults.items():

+             setattr(self, attr, kwargs.pop(attr, default))

+         for kw in kwargs:

+             raise TypeError("Invalid argument: %s" % kw)

+ 

+     def to_dict(self):

+         """

+         Returns:

+             (dict): A JS-ready representation of the parameter.

+         """

+         return dict(

+             name=self.name,

+             label=self.label,

+             default=self.default,

+             renderTag=self.render_tag,

+             renderType=self.render_type,

+             help=self.help,

+             )

@@ -18,6 +18,11 @@ 

  import six

  

  

+ def Noop(value):

+     """Does no validation, just return the value."""

+     return value

+ 

+ 

  def Required(value):

      """Raises an error if the value is ``False``-like."""

      if not bool(value):

Currently if you need anything different from an input field for your widget parameter, you need to setup a React-based widget. It's a bit sad. This PR lets you use, for example, textarea tags for your parameters by just setting render_tag = "textarea" in your parameter definition.

Do these need to be class attributes? It's a bit confusing that they are class attributes here but become instance attributes below (the call to setattr). Is the goal to have these be defaults? If so, I recommend setting them before the for loop in __init__().

Why not just have __init__() explicitly accepts these parameters with defaults?

I recommend adding docblocks to to_json() and __init__().

Just a few optional suggestions, feel free to merge!

rebased onto b4a38c0f1f308d9c0c4269a32926c1cd92f98f31

6 years ago

Alright, I applied your suggestions, thanks for the review!

rebased onto 452afef

6 years ago

Pull-Request has been merged by abompard

6 years ago