From feffda99d7a1ca665f6aa29d3d9ab5c99bede190 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 12 Aug 2026 11:57:00 -0500 Subject: [PATCH] #22447: Pre-release QA (#22908) * Fixes #22447: Pre-release QA Add the missing `cooling_outflow` GraphQL filter on CoolingIntake, so an intake can be filtered by its upstream outflow. CoolingOutflow already exposes the reverse `cooling_intake` filter and the REST filterset already carries `cooling_outflow_id`; the GraphQL intake filter was the only side missing it. Correct the CoolingIntake docstring, which referenced a direct CoolingFeed relationship that does not exist. The serving feed is derived from the device's rack, not stored on the intake. * Fixes #22447: Pre-release QA (filter form + table parity) Address the same-class gaps surfaced in review, all mirror images of the intake/outflow filter parity already fixed: Add the `cooling_intake_id` filter to CoolingOutflowFilterForm and CoolingOutflowTemplateFilterForm. The underlying filtersets already carried `cooling_intake_id` and GraphQL supported it, but the list-view filter panel did not expose it, so an outflow could not be filtered by its downstream intake from the UI. Add `cooling_outflow` to the default columns on CoolingIntakeTable and DeviceCoolingIntakeTable. The outflow tables already default-show `cooling_intake`; the intake tables hid the reverse, so the same relationship displayed inconsistently between the two sides. Note in the CoolingIntake docstring why CoolingIntakeTemplate has no upstream-outflow field: an intake's outflow normally lives on a different device (a CDU), which a device-type template cannot express. --- netbox/dcim/forms/filtersets.py | 14 ++++++++++++-- netbox/dcim/graphql/filters.py | 4 ++++ netbox/dcim/models/device_components.py | 7 ++++++- netbox/dcim/tables/cooling.py | 4 ++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index d9b6f101c..15c7cb649 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1940,7 +1940,7 @@ class CoolingOutflowFilterForm(DeviceComponentFilterForm): fieldsets = ( FieldSet('q', 'filter_id', 'tag'), FieldSet( - 'name', 'label', 'type', 'diameter', 'diameter_unit', name=_('Attributes') + 'name', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake_id', name=_('Attributes') ), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1963,6 +1963,11 @@ class CoolingOutflowFilterForm(DeviceComponentFilterForm): choices=DiameterUnitChoices, required=False ) + cooling_intake_id = DynamicModelMultipleChoiceField( + label=_('Cooling intake'), + queryset=CoolingIntake.objects.all(), + required=False + ) tag = TagFilterField(model) @@ -1970,7 +1975,7 @@ class CoolingOutflowTemplateFilterForm(ModularDeviceComponentTemplateFilterForm) model = CoolingOutflowTemplate fieldsets = ( FieldSet('q', 'filter_id', 'tag'), - FieldSet('name', 'label', 'type', 'diameter', 'diameter_unit', name=_('Attributes')), + FieldSet('name', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake_id', name=_('Attributes')), FieldSet('device_type_id', 'module_type_id', name=_('Device')), ) type = forms.MultipleChoiceField( @@ -1987,6 +1992,11 @@ class CoolingOutflowTemplateFilterForm(ModularDeviceComponentTemplateFilterForm) choices=DiameterUnitChoices, required=False ) + cooling_intake_id = DynamicModelMultipleChoiceField( + label=_('Cooling intake'), + queryset=CoolingIntakeTemplate.objects.all(), + required=False + ) class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 9bcf5320e..3e2508d50 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -1068,6 +1068,10 @@ class CoolingIntakeFilter(ModularComponentFilterMixin, NetBoxModelFilter): max_flow_unit: BaseFilterLookup[ Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')] ] | None = strawberry_django.filter_field() + cooling_outflow: Annotated['CoolingOutflowFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cooling_outflow_id: ID | None = strawberry_django.filter_field() @register_filter(models.CoolingIntakeTemplate, lookups=True) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index a46fad9de..dc60a9eac 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -700,7 +700,12 @@ class CoolingIntake( ): """ A coolant intake port within a Device (e.g. a server cold-plate inlet or CDU intake). A - CoolingIntake is supplied by an upstream CoolingOutflow or CoolingFeed. + CoolingIntake is supplied by an upstream CoolingOutflow. The serving CoolingFeed is + derived from the Device's Rack rather than referenced directly. + + Unlike CoolingOutflow (whose parent intake is on the same Device and can therefore be + templated), an intake's upstream outflow typically lives on a different Device (e.g. a + CDU), so there is deliberately no upstream-outflow field on CoolingIntakeTemplate. """ type = models.CharField( verbose_name=_('type'), diff --git a/netbox/dcim/tables/cooling.py b/netbox/dcim/tables/cooling.py index ab623fc80..7587f6d00 100644 --- a/netbox/dcim/tables/cooling.py +++ b/netbox/dcim/tables/cooling.py @@ -166,7 +166,7 @@ class CoolingIntakeTable(ModularDeviceComponentTable): ) default_columns = ( 'pk', 'name', 'device', 'label', 'type', 'diameter', 'max_flow', - 'description', + 'cooling_outflow', 'description', ) @@ -281,7 +281,7 @@ class DeviceCoolingIntakeTable(CoolingIntakeTable): ) default_columns = ( 'pk', 'name', 'label', 'type', 'diameter', 'max_flow', - 'description', + 'cooling_outflow', 'description', )