Migration to Generated Table Filters
This guide documents the transition from manually maintained table filters to automatically generated filters based on the OpenAPI schema.
Motivation & Vision
The Problem
Manually writing filter components for every API endpoint leads to:
- Boilerplate: Repetitive definitions of select fields, async paginators, and state management.
- Inconsistency: Discrepancies between the frontend filters and the actual API parameters (e.g., incorrect query param names, missing options).
- Maintenance Burden: When API changes (new filters, renamed parameters), developers must manually update the frontend code.
The Solution
We generate filter components directly from the OpenAPI schema (schema.json). This ensures:
- Single Source of Truth: The frontend filters always match the API definition.
- Type Safety: Generated code uses TypeScript interfaces inferred from the schema.
- Automatic Updates: Regenerating filters updates them to reflect API changes instantly.
- Standardization: All filters use consistent UI components (
<Select>,<AsyncPaginate>, etc.) and behavior.
Architecture
The generation process is driven by:
generate-filters.cjs: The Node.js script that parses the schema and outputs React components.generate-filters-config.yaml: A configuration file for customization (overrides, ordering, labels).waldur-js-client: Provides the TypeScript types and API client functions used by the generated code.
Generated Output
The script produces src/table/generated/{OperationId}Filter.tsx files. Each file exports:
{Name}Filter: The filter component containing autonomous filter components (StringFilter, SelectFilter, etc.) that connect directly to Redux.{Name}FilterFormData: TypeScript interface for filter values.{Name}FilterProps: TypeScript interface for component props.select{Name}Filter: A selector to transform Redux filter values into API query parameters.{Name}FilterFormId: A constant string used as the table ID (Redux key).
Migration Process
To migrate a manual filter to a generated one, follow these steps:
1. Identify the OpenAPI Operation
Find the operationId for the list endpoint you are filtering.
Example: For GET /api/customers/, the operation ID might be customers_list.
2. Configure generate-filters-config.yaml
Add an entry for the operation if it doesn't exist. You can specify which filters to include (whitelist) or leave it empty to include all non-excluded filters.
1 2 3 4 5 6 7 8 9 | |
3. Run the Generator
Execute the generation script:
1 | |
This will create/update src/table/generated/CustomersFilter.tsx.
4. Replace the Manual Component
In your table definition (e.g., CustomersList.tsx), replace the manual filter component with the generated one. Use useFilterValues to get the Redux state and the generated selector to prepare the filter object for useTable.
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
After:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
5. Verify & Clean Up
- Check if the new filter behaves correctly in the UI.
- Verify that types are correct.
- Delete the old manual filter file.
Configuration & Customization (generate-filters-config.yaml)
You can customize almost every aspect of the generated filters.
Key Configuration Options
| Option | Description | Example |
|---|---|---|
label |
Custom label for the filter. | label: "Organization" |
component |
React component to use. | component: "Autocomplete" |
loadOptions |
API method for async loading. | loadOptions: "customersList" |
valueField |
Field to use as value. | valueField: "uuid" |
labelField |
Field to show in UI. | labelField: "name" |
mapTo |
Map a renamed filter back to a specific API param. | mapTo: "organization_group_uuid" |
options |
Hardcoded options for Select. | options: [{ label: "Yes", value: true }] |
Example Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Tips & Quirks
1. Renaming and _uuid Suffix
The generator automatically drops _uuid suffixes from filter names for cleaner code (e.g., customer_uuid becomes customer).
- Quirk: If you list filters in the
filterswhitelist in YAML, you must use the renamed name (e.g.,customer, notcustomer_uuid), OR the original name ifmapTois correctly inferred. - Tip: The generator logic handles the mapping back to the original parameter name in the
select{Name}Filterselector.
2. Explicit Typing
Generated components are strictly typed.
- Props:
FunctionComponent<FilterProps>defines what props the filter accepts. - Form Data:
FilterFormDatadefines the shape of the form values. - Callbacks:
getOptionLabel/getOptionValuehave explicit type annotations (e.g.,(option: Customer) => option.name).
3. Autocomplete vs. Select
- Autocomplete: Uses
AsyncPaginate. RequiresloadOptions,valueField, andlabelFieldto be set or inferred. - Select: Uses standard
Select. Used for enums, booleans, or fixed options.
4. Handling Props (props.*)
If a filter's options come from parent props (not an API call), use the props. prefix in configuration.
1 2 3 | |
The generator will:
- Infer the
FilterPropsinterface containingprojectRoles. - Pass
propsto the component. - Render
options={props.projectRoles}.
Troubleshooting
"My filter is missing"
- Check if it's in the
GenerateFiltersCIexclusion list ingenerate-filters.cjs. - Check if it's referenced in the
yamlwhitelist correctly.
"My filter is a StringField but should be Autocomplete"
- This usually means the schema inference didn't detect it as a relation.
- Fix: Add an override in
generate-filters-config.yamlsettingcomponent: AutocompleteandloadOptions.
"TypeScript errors in generated code"
- Run
node generate-filters.cjsto ensure the code is fresh. - Check if
waldur-js-clientexports the types you expect.