diff --git a/api/features/serializers.py b/api/features/serializers.py index 55785e1bda5a..d922dd11ae47 100644 --- a/api/features/serializers.py +++ b/api/features/serializers.py @@ -90,6 +90,10 @@ class FeatureQuerySerializer(serializers.Serializer): # type: ignore[type-arg] required=False, help_text="Integer ID of the environment to view features in the context of.", ) + segment = serializers.IntegerField( + required=False, + help_text="Integer ID of the segment to retrieve segment overrides for.", + ) is_enabled = serializers.BooleanField( allow_null=True, required=False, @@ -141,6 +145,7 @@ class CreateFeatureSerializer(DeleteBeforeUpdateWritableNestedModelSerializer): group_owners = UserPermissionGroupSummarySerializer(many=True, read_only=True) environment_feature_state = serializers.SerializerMethodField() + segment_feature_state = serializers.SerializerMethodField() num_segment_overrides = serializers.SerializerMethodField( help_text="Number of segment overrides that exist for the given feature " @@ -188,6 +193,7 @@ class Meta: "uuid", "project", "environment_feature_state", + "segment_feature_state", "num_segment_overrides", "num_identity_overrides", "is_num_identity_overrides_complete", @@ -296,6 +302,17 @@ def get_environment_feature_state( # type: ignore[return] ): return FeatureStateSerializerSmall(instance=feature_state).data + @swagger_serializer_method( # type: ignore[misc] + serializer_or_field=FeatureStateSerializerSmall(allow_null=True) + ) + def get_segment_feature_state( # type: ignore[return] + self, instance: Feature + ) -> dict[str, Any] | None: + if (segment_feature_states := self.context.get("segment_feature_states")) and ( + segment_feature_state := segment_feature_states.get(instance.id) + ): + return FeatureStateSerializerSmall(instance=segment_feature_state).data + def get_num_segment_overrides(self, instance: Feature) -> int: try: return self.context["overrides_data"][instance.id].num_segment_overrides # type: ignore[no-any-return] @@ -645,6 +662,13 @@ class SDKFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[t ) +class EnvironmentFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[type-arg] + segment = serializers.IntegerField( + required=False, + help_text="ID of the segment to filter segment overrides by.", + ) + + class CustomCreateSegmentOverrideFeatureStateSerializer( CreateSegmentOverrideFeatureStateSerializer ): diff --git a/api/features/views.py b/api/features/views.py index 30464c980859..410aa3c0068d 100644 --- a/api/features/views.py +++ b/api/features/views.py @@ -182,18 +182,33 @@ def get_queryset(self): # type: ignore[no-untyped-def] page = self.paginate_queryset(queryset) self.environment = Environment.objects.get(id=environment_id) self.feature_ids = [feature.id for feature in page] - q = Q( + feature_states_query = Q( feature_id__in=self.feature_ids, identity__isnull=True, feature_segment__isnull=True, ) feature_states = get_environment_flags_list( environment=self.environment, - additional_filters=q, + additional_filters=feature_states_query, additional_select_related_args=["feature_state_value", "feature"], ) self._feature_states = {fs.feature_id: fs for fs in feature_states} + if segment_id := query_data.get("segment"): + segment_query = Q( + feature_id__in=self.feature_ids, + identity__isnull=True, + feature_segment__segment_id=segment_id, + ) + segment_feature_states = get_environment_flags_list( + environment=self.environment, + additional_filters=segment_query, + additional_select_related_args=["feature_state_value", "feature"], + ) + self._segment_feature_states = { + fs.feature_id: fs for fs in segment_feature_states + } + return queryset def paginate_queryset(self, queryset: QuerySet[Feature]) -> list[Feature]: # type: ignore[override] @@ -225,9 +240,13 @@ def get_serializer_context(self): # type: ignore[no-untyped-def] return context feature_states = getattr(self, "_feature_states", {}) + segment_feature_states = getattr(self, "_segment_feature_states", {}) project = get_object_or_404(Project.objects.all(), pk=self.kwargs["project_pk"]) context.update( - project=project, user=self.request.user, feature_states=feature_states + project=project, + user=self.request.user, + feature_states=feature_states, + segment_feature_states=segment_feature_states, ) if self.action == "list" and "environment" in self.request.query_params: @@ -664,6 +683,7 @@ class EnvironmentFeatureStateViewSet(BaseFeatureStateViewSet): def get_queryset(self): # type: ignore[no-untyped-def] queryset = super().get_queryset().filter(feature_segment=None) # type: ignore[no-untyped-call] + if "anyIdentity" in self.request.query_params: # TODO: deprecate anyIdentity query parameter return queryset.exclude(identity=None) diff --git a/api/tests/unit/features/test_unit_features_views.py b/api/tests/unit/features/test_unit_features_views.py index e4d5de95dbb6..c53a59e1234a 100644 --- a/api/tests/unit/features/test_unit_features_views.py +++ b/api/tests/unit/features/test_unit_features_views.py @@ -1595,8 +1595,6 @@ def test_environment_feature_states_does_not_return_null_versions( assert len(response_json["results"]) == 1 assert response_json["results"][0]["id"] == feature_state.id - # Feature tests - def test_create_feature_default_is_archived_is_false( admin_client_new: APIClient, project: Project @@ -4077,6 +4075,76 @@ def test_get_multivariate_options_feature_not_found_responds_404( assert response.status_code == status.HTTP_404_NOT_FOUND +def test_list_features_segment_query_param_with_valid_segment( + admin_client_new: APIClient, + project: Project, + feature: Feature, + environment: Environment, + segment: Segment, +) -> None: + # Given + feature_segment = FeatureSegment.objects.create( + feature=feature, + segment=segment, + environment=environment, + ) + segment_override = FeatureState.objects.create( + feature=feature, + feature_segment=feature_segment, + environment=environment, + enabled=True, + ) + segment_override.feature_state_value.string_value = "segment_value" + segment_override.feature_state_value.save() + + base_url = reverse("api-v1:projects:project-features-list", args=[project.id]) + url = f"{base_url}?environment={environment.id}&segment={segment.id}" + + # When + response = admin_client_new.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + response_json = response.json() + feature_data = next( + filter(lambda r: r["id"] == feature.id, response_json["results"]) + ) + + assert "environment_feature_state" in feature_data + segment_state = feature_data["segment_feature_state"] + + assert segment_state is not None + assert segment_state["id"] == segment_override.id + assert segment_state["feature_state_value"] == "segment_value" + assert segment_state["enabled"] is True + + +def test_list_features_segment_query_param_with_invalid_segment( + admin_client_new: APIClient, + project: Project, + feature: Feature, + environment: Environment, + segment: Segment, +) -> None: + # Given + base_url = reverse("api-v1:projects:project-features-list", args=[project.id]) + invalid_segment_id = segment.id + 9999 + url = f"{base_url}?environment={environment.id}&segment={invalid_segment_id}" + + # When + response = admin_client_new.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + response_json = response.json() + feature_data = next( + filter(lambda r: r["id"] == feature.id, response_json["results"]) + ) + + assert "segment_feature_state" in feature_data + assert feature_data["segment_feature_state"] is None + + def test_create_multiple_features_with_metadata_keeps_metadata_isolated( admin_client_new: APIClient, project: Project, diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 06caf5cbb209..7f22ce38ba7e 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -1757,9 +1757,10 @@ const CreateFlag = class extends Component { e.stopPropagation() removeUserOverride( { - cb: () => + cb: () => this.userOverridesPage( - 1, true + 1, + true, ), environmentId: this.props diff --git a/frontend/web/components/tags/Tag.tsx b/frontend/web/components/tags/Tag.tsx index 886b9c701eee..ba1739caa68b 100644 --- a/frontend/web/components/tags/Tag.tsx +++ b/frontend/web/components/tags/Tag.tsx @@ -66,7 +66,7 @@ const Tag: FC = ({ selected, tag, }) => { - const shouldLighten = (color: Color) => getDarkMode() && color.isDark(); + const shouldLighten = (color: Color) => getDarkMode() && color.isDark() const tagColor = Utils.colour(getTagColor(tag, selected)) if (isDot) { return (