Implementing Custom Marketplace Option Types
This guide explains how to add new option types to Waldur's marketplace offering system, using the conditional_cascade implementation as a reference.
Overview
Waldur marketplace options allow service providers to define custom form fields for their offerings. The system supports various built-in types like string, select_string, boolean, etc., and can be extended with custom types.
Architecture
The marketplace options system consists of several components:
- Backend: Option type validation, serialization, and storage
- Admin Interface: Configuration UI for service providers
- User Interface: Form fields displayed to users during ordering
- Form Processing: Attribute handling during order creation
Implementation Steps
1. Backend: Add Field Type Constant
Add your new type to the FIELD_TYPES constant:
File: src/waldur_mastermind/marketplace/serializers.py
1 2 3 4 5 6 7 | |
2. Backend: Create Configuration Serializers
Define serializers for validating your option configuration:
File: src/waldur_mastermind/marketplace/serializers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
3. Backend: Add Order Validation Support
Register your field type for order processing:
File: src/waldur_mastermind/common/serializers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
4. Frontend: Add Type Constant
Add the new type to the frontend constants:
File: src/marketplace/offerings/update/options/constants.ts
1 2 3 4 5 6 7 8 | |
5. Frontend: Create Configuration Component
Create an admin configuration component:
File: src/marketplace/offerings/update/options/YourCustomConfiguration.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
6. Frontend: Create User-Facing Component
Create the component that users see in order forms:
File: src/marketplace/common/YourCustomField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 | |
7. Frontend: Update Configuration Forms
Add your type to the option configuration form:
File: src/marketplace/offerings/update/options/OptionForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
8. Frontend: Update Order Form Rendering
Add your field to the order form renderer:
File: src/marketplace/common/OptionsForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
9. Frontend: Handle Form Data Processing
Update form utilities if needed:
File: src/marketplace/offerings/store/utils.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
File: src/marketplace/details/utils.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
10. Testing
Create comprehensive tests for your new option type:
File: src/waldur_mastermind/marketplace/tests/test_your_custom_type.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
Key Considerations
Data Format Consistency
- Configuration Phase: How admins configure the option (JSON strings for complex data)
- Display Phase: How the option is displayed in forms (parsed objects)
- Submission Phase: What format users submit (depends on your UI component)
- Storage Phase: How the data is stored in orders/resources (final format)
Error Handling
- Ensure all error dictionaries use string keys for JSON serialization compatibility
- Provide clear, actionable error messages
- Handle edge cases (empty values, malformed data, etc.)
Form Integration
- React-final-form compatibility: For configuration and user interfaces
- FormContainer integration: For most user order forms
Performance
- Use
useCallbackanduseRefto prevent unnecessary re-renders - Avoid object dependencies in
useEffectthat cause infinite loops - Memoize expensive computations
Example: Conditional Cascade Implementation
The conditional_cascade type demonstrates all these concepts:
Backend Components
CascadeStepSerializer- Validates individual steps with JSON parsingCascadeConfigSerializer- Validates overall configuration with dependency checkingConditionalCascadeField(in common/serializers.py) - Handles order validation
Frontend Components
ConditionalCascadeConfiguration- Admin configuration interfaceConditionalCascadeWidget- Admin form componentConditionalCascadeField- User order form component
Key Features
- Cascading Dependencies: Dropdowns that depend on previous selections
- JSON Configuration: Complex configuration stored as JSON strings
- Object Preservation: Keeps selection objects intact through form processing
- Bidirectional Sync: Proper state management between form and component
Testing Strategy
Create tests covering:
- Configuration Validation - Valid/invalid option configurations
- Order Processing - Attribute validation during order creation
- Edge Cases - Unicode, special characters, empty values, malformed data
- Error Handling - JSON serialization compatibility, clear error messages
- Integration - Mixed field types, form submission end-to-end
Best Practices
- Follow Existing Patterns - Study similar option types before implementing
- Incremental Development - Implement backend validation first, then frontend
- Comprehensive Testing - Test all data paths and edge cases
- Error Prevention - Use TypeScript interfaces and runtime validation
- Documentation - Document configuration format and usage examples
Common Pitfalls
- JSON Serialization Errors - Always use string keys in error dictionaries
- Infinite Re-renders - Avoid objects in useEffect dependencies
- Form Integration Issues - Ensure proper
inputprop handling - Data Format Mismatches - Handle format differences between config/display/submission
- Validation Bypass - Don't forget to add your type to
FIELD_CLASSESmapping
Update Frontend Type Handlers
Add your new type to the OptionValueRenders object in the frontend:
File: src/marketplace/resources/options/OptionValue.tsx
1 2 3 4 | |
Important: If this step is missed, TypeScript compilation will fail with:
1 | |
Following this guide ensures your custom option type integrates seamlessly with Waldur's marketplace system and provides a consistent user experience.
Built-in Option Types
Component Multiplier
The component_multiplier option type allows users to input a value that gets automatically multiplied by a configurable factor to set limits for limit-based offering components.
Use Case
Perfect for scenarios where users need to specify resources in user-friendly units that need conversion:
- Storage: User enters "2 TB", automatically sets 100,000 inodes (2 × 50,000)
- Compute: User enters "4 cores", automatically sets 16 GB RAM (4 × 4)
- Network: User enters "100 Mbps", automatically sets bandwidth limits in bytes
Configuration
Backend Configuration (component_multiplier_config):
1 2 3 4 5 6 | |
Option Definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Behavior
- User Input: User enters a value (e.g., "2" for 2 TB)
- Frontend Multiplication: Value is multiplied by factor (2 × 50,000 = 100,000)
- Automatic Limit Setting: The calculated value (100,000) is automatically set as the limit for the specified component (
storage_inodes) - Validation: Frontend validates user input against
min_limitandmax_limitbefore multiplication
The multiplication happens in the order form only; the server stores the entered value as an attribute and does not recalculate any limit from it. To derive limits the server enforces, use Component Formula.
Requirements
- Component Dependency: Must reference an existing limit-based component (
billing_type: "limit") - Factor: Must be a positive integer ≥ 1
- Limits:
min_limitandmax_limitapply to user input, not the calculated result
Implementation Components
- Configuration:
ComponentMultiplierConfiguration.tsx- Admin interface for setting up the multiplier - User Field:
ComponentMultiplierField.tsx- User input field that handles multiplication and limit updates
Component Formula
The component_formula option type asks the customer for one number and sets
one or more limit-based components from it. The customer orders in their own
terms, such as net database capacity, and the offering derives the gross
quantities it bills for.
Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
min and max bound the value the customer enters, not the results.
Formula language
A formula may use only input (the entered value), numbers such as 2 or
0.25, the operators + - * /, unary minus and parentheses. There are no
functions and no other names. Formulas are at most 255 characters long.
Component Sum
The component_sum option type sets a limit-based component to the sum of
other limit-based components. The customer does not fill it in; any value sent
for it is dropped.
1 2 3 4 5 6 7 8 9 10 | |
The summed components may be formula targets, components the customer enters directly, or the targets of other sums.
Derived limit behaviour
- The server calculates the limits. When an order is created, Waldur evaluates the formulas and then the sums, and writes the results into the order's limits. It replaces any value the client sent for a derived component, so the price and the provisioned quantity always follow the configuration. Changing the formula input of a pending order recalculates them the same way.
- Rounding: each result is rounded up to the target component's
limit_decimal_places. It then passes the component's usual checks (minimum, maximum, maximum available), and an error names the component. - No input, no limit: an optional formula left empty, or hidden by
visible_if, derives nothing. A sum is written only when at least one of its components has a value. - Division by zero or a negative result is an order validation error.
- Existing resources: every later change to a resource's limits (limit update, limit change request, renewal, plan switch) recalculates the derived limits from the inputs recorded on the resource. A derived value sent by the client is replaced and one left out is put back, and a sum follows the components it adds up. A resource ordered before the option existed has no recorded input and keeps its current derived values. Derived limits cannot be reallocated between resources.
- Provider approval: a provider who changes a formula input while approving an order changes its derived limits and price with it.
- Order options only:
component_sumcannot be a resource option;component_formulacan only as described below.
Changing the input after ordering
To let customers change the value after ordering, add a resource option
of type component_formula with the same internal name as the order option:
1 2 3 4 5 6 7 8 | |
- It has no formulas of its own: the order option's are used, and its
minandmaxare copied from the order option when the offering is saved. Acomponent_formularesource option without such an order option is refused, and so is removing or retyping an order option that one pairs with. The resource option itself cannot be removed, or re-paired under another name, while a resource of the offering holds a value changed since ordering: without the pairing, that resource's limits would fall back to the ordered value on their next change. - The value entered at order time is copied onto the resource, so it shows on the resource's Options tab (resources ordered earlier show the value from their order).
- Changing it through
update_optionsalways creates an UPDATE order, whatevercreate_orders_on_resource_option_changesays, carrying the new value (new_options) and the recalculated limits (old_limitsandlimits), so it is priced, approved and provisioned like any limit change. When completed, the new value and the new limits are applied together. - A provider who changes the value while approving the order changes the
limits and price with it.
update_options_directrefuses to change it, since that would bypass the order. - Later limit changes calculate from the current value: the resource option when set, otherwise the value from the order.
- Defaults and bounds: a formula option's
defaultis used when the input is omitted, andmin/maxare enforced on every path that changes the input, not only on order creation.
Validation when the offering is saved
- Every formula must parse under the language above.
- Every component a formula or sum names must be a limit-based component of
the offering (
billing_type: limit; prepaid one-time components are not accepted). - While an option refers to a component, the component cannot be removed, renamed or switched to another billing type. If a plan's billing mode stops billing it as a limit, orders on that plan are refused with an error naming the option.
- A component may be derived by only one option.
- A sum may not include its own target, and sums may not form a cycle.