You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2025/11/04 21:58:52 UTC

(nifi) branch main updated: NIFI-14996 Added support to Clear Component Bulletins (#10409)

This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 2ee74101c5 NIFI-14996 Added support to Clear Component Bulletins (#10409)
2ee74101c5 is described below

commit 2ee74101c58e0831acbcef233cecef3ac718400a
Author: Matt Gilman <ma...@gmail.com>
AuthorDate: Tue Nov 4 16:58:47 2025 -0500

    NIFI-14996 Added support to Clear Component Bulletins (#10409)
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../org/apache/nifi/web/api/dto/BulletinDTO.java   |  18 +
 .../nifi/web/api/dto/util/InstantAdapter.java      |   2 -
 .../apache/nifi/web/api/entity/BulletinEntity.java |  18 +
 .../ClearBulletinsForGroupRequestEntity.java       |  78 ++
 .../ClearBulletinsForGroupResultsEntity.java}      |  28 +-
 .../api/entity/ClearBulletinsRequestEntity.java    |  50 ++
 .../web/api/entity/ClearBulletinsResultEntity.java |  69 ++
 .../http/StandardHttpResponseMapper.java           |   4 +
 .../endpoints/ClearBulletinsEndpointMerger.java    |  84 +++
 .../ClearBulletinsForGroupEndpointMerger.java      |  52 ++
 .../ClearBulletinsEndpointMergerTest.java          | 114 +++
 .../ClearBulletinsForGroupEndpointMergerTest.java  |  74 ++
 .../nifi/events/VolatileBulletinRepository.java    |  49 ++
 .../events/VolatileBulletinRepositoryTest.java     | 270 +++++++
 .../org/apache/nifi/web/NiFiServiceFacade.java     |  36 +
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  56 +-
 .../apache/nifi/web/api/ControllerResource.java    | 202 ++++-
 .../nifi/web/api/ControllerServiceResource.java    |  74 ++
 .../java/org/apache/nifi/web/api/FlowResource.java | 126 +++-
 .../org/apache/nifi/web/api/InputPortResource.java |  70 ++
 .../apache/nifi/web/api/OutputPortResource.java    |  70 ++
 .../nifi/web/api/ParameterProviderResource.java    |  74 ++
 .../org/apache/nifi/web/api/ProcessorResource.java |  74 ++
 .../nifi/web/api/RemoteProcessGroupResource.java   |  75 ++
 .../apache/nifi/web/api/ReportingTaskResource.java |  74 ++
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |   1 +
 .../org/apache/nifi/web/api/dto/EntityFactory.java |   1 +
 .../nifi/web/StandardNiFiServiceFacadeTest.java    | 215 ++++++
 .../service/canvas-actions.service.spec.ts         | 441 +++++++++++
 .../service/canvas-actions.service.ts              | 101 ++-
 .../service/canvas-context-menu.service.ts         |   6 +
 .../service/canvas-utils.service.spec.ts           | 258 ++++++-
 .../flow-designer/service/canvas-utils.service.ts  |   6 +
 .../service/controller-service.service.ts          |   7 +
 .../pages/flow-designer/service/flow.service.ts    |  27 +-
 .../controller-services.actions.ts                 |  12 +
 .../controller-services.effects.spec.ts            | 451 +++++++++++-
 .../controller-services.effects.ts                 |  26 +
 .../controller-services.reducer.spec.ts            | 434 +++++++++++
 .../controller-services.reducer.ts                 |  13 +
 .../pages/flow-designer/state/flow/flow.actions.ts |  34 +-
 .../flow-designer/state/flow/flow.effects.spec.ts  |  94 ++-
 .../pages/flow-designer/state/flow/flow.effects.ts |  67 ++
 .../flow-designer/state/flow/flow.reducer.spec.ts  | 355 +++++++++
 .../pages/flow-designer/state/flow/flow.reducer.ts |  30 +-
 .../app/pages/flow-designer/state/flow/index.ts    |  14 +
 .../controller-services.component.html             |   5 +-
 .../controller-services.component.spec.ts          | 276 ++++++-
 .../controller-services.component.ts               |  24 +-
 .../settings/service/flow-analysis-rule.service.ts |   7 +
 .../management-controller-service.service.ts       |   7 +
 .../settings/service/parameter-provider.service.ts |   7 +
 .../settings/service/registry-client.service.ts    |   7 +
 .../settings/service/reporting-task.service.ts     |   7 +
 .../flow-analysis-rules.actions.ts                 |  12 +-
 .../flow-analysis-rules.effects.spec.ts            | 638 +++++++++++++---
 .../flow-analysis-rules.effects.ts                 |  33 +
 .../flow-analysis-rules.reducer.spec.ts            | 180 +++++
 .../flow-analysis-rules.reducer.ts                 |  13 +
 .../management-controller-services.actions.ts      |  12 +
 .../management-controller-services.effects.spec.ts | 340 ++++++---
 .../management-controller-services.effects.ts      |  33 +
 .../management-controller-services.reducer.spec.ts | 221 ++++++
 .../management-controller-services.reducer.ts      |  13 +
 .../settings/state/parameter-providers/index.ts    |  10 +-
 .../parameter-providers.actions.ts                 |  11 +
 .../parameter-providers.effects.spec.ts            | 631 +++++++++++++---
 .../parameter-providers.effects.ts                 |  33 +
 .../parameter-providers.reducer.spec.ts            | 194 +++++
 .../parameter-providers.reducer.ts                 |  15 +-
 .../registry-clients/registry-clients.actions.ts   |  11 +
 .../registry-clients.effects.spec.ts               | 562 ++++++++++----
 .../registry-clients/registry-clients.effects.ts   |  33 +
 .../registry-clients.reducer.spec.ts               | 174 +++++
 .../registry-clients/registry-clients.reducer.ts   |  13 +
 .../reporting-tasks/reporting-tasks.actions.ts     |  12 +-
 .../reporting-tasks.effects.spec.ts                | 638 +++++++++++++---
 .../reporting-tasks/reporting-tasks.effects.ts     |  33 +
 .../reporting-tasks.reducer.spec.ts                | 201 +++++
 .../reporting-tasks/reporting-tasks.reducer.ts     |  13 +
 .../flow-analysis-rule-table.component.html        |   6 +
 .../flow-analysis-rule-table.component.spec.ts     | 809 ++++++++++++++++++++-
 .../flow-analysis-rule-table.component.ts          |  10 +
 .../flow-analysis-rules.component.html             |   5 +-
 .../flow-analysis-rules.component.spec.ts          | 375 +++++++++-
 .../flow-analysis-rules.component.ts               |  24 +-
 .../management-controller-services.component.html  |   5 +-
 ...anagement-controller-services.component.spec.ts | 274 ++++++-
 .../management-controller-services.component.ts    |  24 +-
 .../parameter-providers-table.component.html       |  16 +-
 .../parameter-providers-table.component.scss       |   2 +-
 .../parameter-providers-table.component.spec.ts    | 260 ++++++-
 .../parameter-providers-table.component.ts         |  26 +-
 .../parameter-providers.component.html             |   5 +-
 .../parameter-providers.component.spec.ts          | 294 +++++++-
 .../parameter-providers.component.ts               |  23 +-
 .../registry-client-table.component.html           |  12 +-
 .../registry-client-table.component.spec.ts        | 427 ++++++++++-
 .../registry-client-table.component.ts             |  10 +
 .../registry-clients.component.html                |   3 +-
 .../registry-clients.component.spec.ts             | 218 +++++-
 .../registry-clients/registry-clients.component.ts |  25 +-
 .../reporting-task-table.component.html            |   6 +
 .../reporting-task-table.component.spec.ts         | 666 ++++++++++++++++-
 .../reporting-task-table.component.ts              |   9 +
 .../reporting-tasks/reporting-tasks.component.html |   3 +-
 .../reporting-tasks.component.spec.ts              | 392 +++++++++-
 .../reporting-tasks/reporting-tasks.component.ts   |  24 +-
 .../apps/nifi/src/app/state/shared/index.ts        |  15 +
 .../controller-service-table.component.html        |   6 +
 .../controller-service-table.component.spec.ts     | 210 +++++-
 .../controller-service-table.component.ts          |  10 +
 .../disable-controller-service.component.spec.ts   |   7 +-
 .../enable-controller-service.component.spec.ts    |   4 +-
 .../shared/src/services/nifi-common.service.ts     |  32 +-
 .../main/frontend/libs/shared/src/types/index.ts   |   2 +
 .../apache/nifi/util/MockBulletinRepository.java   |  14 +
 117 files changed, 12312 insertions(+), 794 deletions(-)

diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
index 51ee6b6388..da7136dadd 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
@@ -17,10 +17,12 @@
 package org.apache.nifi.web.api.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.nifi.web.api.dto.util.InstantAdapter;
 import org.apache.nifi.web.api.dto.util.TimeAdapter;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
 import java.util.Date;
 
 /**
@@ -38,6 +40,7 @@ public class BulletinDTO {
     private String level;
     private String message;
     private Date timestamp;
+    private Instant timestampIso;
     private String sourceType;
     private String stackTrace;
 
@@ -160,6 +163,21 @@ public class BulletinDTO {
         this.timestamp = timestamp;
     }
 
+    /**
+     * @return When this bulletin was generated in ISO format with full date and milliseconds.
+     */
+    @XmlJavaTypeAdapter(InstantAdapter.class)
+    @Schema(description = "When this bulletin was generated in ISO format with full date and milliseconds.",
+            type = "string"
+    )
+    public Instant getTimestampIso() {
+        return timestampIso;
+    }
+
+    public void setTimestampIso(Instant timestampIso) {
+        this.timestampIso = timestampIso;
+    }
+
     @Schema(description = "The type of the source component"
     )
     public String getSourceType() {
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java
index 9bc7de3fc8..5692085579 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java
@@ -24,8 +24,6 @@ import java.time.Instant;
  */
 public class InstantAdapter extends XmlAdapter<String, Instant> {
 
-    public static final String DEFAULT_DATE_TIME_FORMAT = "MM/dd/yyyy HH:mm:ss z";
-
     @Override
     public String marshal(Instant instant) throws Exception {
         return instant.toString();
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java
index 435b5daefb..4eeb71c5db 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java
@@ -19,10 +19,12 @@ package org.apache.nifi.web.api.entity;
 import io.swagger.v3.oas.annotations.media.Schema;
 import org.apache.nifi.web.api.dto.BulletinDTO;
 import org.apache.nifi.web.api.dto.ReadablePermission;
+import org.apache.nifi.web.api.dto.util.InstantAdapter;
 import org.apache.nifi.web.api.dto.util.TimeAdapter;
 
 import jakarta.xml.bind.annotation.XmlRootElement;
 import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
 import java.util.Date;
 
 /**
@@ -35,6 +37,7 @@ public class BulletinEntity extends Entity implements ReadablePermission {
     private String groupId;
     private String sourceId;
     private Date timestamp;
+    private Instant timestampIso;
     private String nodeAddress;
     private Boolean canRead;
 
@@ -101,6 +104,21 @@ public class BulletinEntity extends Entity implements ReadablePermission {
         this.timestamp = timestamp;
     }
 
+    /**
+     * @return When this bulletin was generated in ISO format with full date and milliseconds.
+     */
+    @Schema(description = "When this bulletin was generated in ISO format with full date and milliseconds.",
+            type = "string"
+    )
+    @XmlJavaTypeAdapter(InstantAdapter.class)
+    public Instant getTimestampIso() {
+        return timestampIso;
+    }
+
+    public void setTimestampIso(Instant timestampIso) {
+        this.timestampIso = timestampIso;
+    }
+
     /**
      * @return If clustered, the address of the node from which the bulletin originated.
      */
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupRequestEntity.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupRequestEntity.java
new file mode 100644
index 0000000000..80cc42af61
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupRequestEntity.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.api.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import org.apache.nifi.web.api.dto.util.InstantAdapter;
+
+import java.time.Instant;
+import java.util.Set;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request to clear bulletins for multiple components.
+ */
+@XmlRootElement(name = "clearBulletinsForGroupRequestEntity")
+public class ClearBulletinsForGroupRequestEntity extends Entity {
+
+    private String id;
+    private Instant fromTimestamp;
+    private Set<String> components;
+
+    /**
+     * @return The id of the ProcessGroup
+     */
+    @Schema(description = "The id of the ProcessGroup"
+    )
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the timestamp from which to clear bulletins (inclusive)
+     */
+    @XmlJavaTypeAdapter(InstantAdapter.class)
+    @Schema(description = "The timestamp from which to clear bulletins (inclusive). This field is required.",
+            type = "string",
+            required = true
+    )
+    public Instant getFromTimestamp() {
+        return fromTimestamp;
+    }
+
+    public void setFromTimestamp(Instant fromTimestamp) {
+        this.fromTimestamp = fromTimestamp;
+    }
+
+    /**
+     * @return The component IDs for which to clear bulletins. If not specified, all authorized descendant components will be used.
+     */
+    @Schema(description = "Optional component IDs for which to clear bulletins. If not specified, all authorized descendant components will be used."
+    )
+    public Set<String> getComponents() {
+        return components;
+    }
+
+    public void setComponents(Set<String> components) {
+        this.components = components;
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupResultsEntity.java
similarity index 51%
copy from nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java
copy to nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupResultsEntity.java
index 9bc7de3fc8..880fc93a7d 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/InstantAdapter.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsForGroupResultsEntity.java
@@ -14,25 +14,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.web.api.dto.util;
+package org.apache.nifi.web.api.entity;
 
-import jakarta.xml.bind.annotation.adapters.XmlAdapter;
-import java.time.Instant;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
 
 /**
- * XmlAdapter for (un)marshalling an Instant
+ * A serialized representation of this class can be placed in the entity body of a response from clearing bulletins for multiple components.
  */
-public class InstantAdapter extends XmlAdapter<String, Instant> {
+@XmlRootElement(name = "clearBulletinsForGroupResultsEntity")
+public class ClearBulletinsForGroupResultsEntity extends Entity {
 
-    public static final String DEFAULT_DATE_TIME_FORMAT = "MM/dd/yyyy HH:mm:ss z";
+    private int bulletinsCleared;
 
-    @Override
-    public String marshal(Instant instant) throws Exception {
-        return instant.toString();
+    /**
+     * @return The total number of bulletins that were cleared
+     */
+    @Schema(description = "The total number of bulletins that were cleared.")
+    public int getBulletinsCleared() {
+        return bulletinsCleared;
     }
 
-    @Override
-    public Instant unmarshal(String instant) throws Exception {
-        return Instant.parse(instant);
+    public void setBulletinsCleared(int bulletinsCleared) {
+        this.bulletinsCleared = bulletinsCleared;
     }
 }
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsRequestEntity.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsRequestEntity.java
new file mode 100644
index 0000000000..bf05132082
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsRequestEntity.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.api.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import org.apache.nifi.web.api.dto.util.InstantAdapter;
+
+import java.time.Instant;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request to clear bulletins for a component.
+ */
+@XmlRootElement(name = "clearBulletinsRequestEntity")
+public class ClearBulletinsRequestEntity extends Entity {
+
+    private Instant fromTimestamp;
+
+    /**
+     * @return the timestamp from which to clear bulletins (inclusive)
+     */
+    @XmlJavaTypeAdapter(InstantAdapter.class)
+    @Schema(description = "The timestamp from which to clear bulletins (inclusive). This field is required.",
+            type = "string",
+            required = true
+    )
+    public Instant getFromTimestamp() {
+        return fromTimestamp;
+    }
+
+    public void setFromTimestamp(Instant fromTimestamp) {
+        this.fromTimestamp = fromTimestamp;
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsResultEntity.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsResultEntity.java
new file mode 100644
index 0000000000..a64ce409b5
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ClearBulletinsResultEntity.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.api.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+import java.util.List;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response from the REST API containing the result of clearing bulletins for a component.
+ */
+@XmlRootElement(name = "clearBulletinsResultEntity")
+public class ClearBulletinsResultEntity extends Entity {
+
+    private int bulletinsCleared;
+    private String componentId;
+    private List<BulletinEntity> bulletins;
+
+    /**
+     * @return the number of bulletins that were cleared
+     */
+    @Schema(description = "The number of bulletins that were cleared.")
+    public int getBulletinsCleared() {
+        return bulletinsCleared;
+    }
+
+    public void setBulletinsCleared(int bulletinsCleared) {
+        this.bulletinsCleared = bulletinsCleared;
+    }
+
+    /**
+     * @return the id of the component for which bulletins were cleared
+     */
+    @Schema(description = "The id of the component for which bulletins were cleared.")
+    public String getComponentId() {
+        return componentId;
+    }
+
+    public void setComponentId(String componentId) {
+        this.componentId = componentId;
+    }
+
+    /**
+     * @return the current bulletins for the component after clearing
+     */
+    @Schema(description = "The current bulletins for the component after clearing.")
+    public List<BulletinEntity> getBulletins() {
+        return bulletins;
+    }
+
+    public void setBulletins(List<BulletinEntity> bulletins) {
+        this.bulletins = bulletins;
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
index 2331d470f8..f16d4eedc2 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
@@ -20,6 +20,8 @@ import jakarta.ws.rs.core.StreamingOutput;
 import org.apache.nifi.cluster.coordination.http.endpoints.AccessPolicyEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.AssetsEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.BulletinBoardEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.ClearBulletinsEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.ClearBulletinsForGroupEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ComponentStateEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ConnectionEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ConnectionStatusEndpointMerger;
@@ -205,6 +207,8 @@ public class StandardHttpResponseMapper implements HttpResponseMapper {
         endpointMergers.add(new NarSummariesEndpointMerger());
         endpointMergers.add(new NarDetailsEndpointMerger());
         endpointMergers.add(new AssetsEndpointMerger());
+        endpointMergers.add(new ClearBulletinsEndpointMerger());
+        endpointMergers.add(new ClearBulletinsForGroupEndpointMerger());
     }
 
     @Override
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMerger.java b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMerger.java
new file mode 100644
index 0000000000..1a760edfb6
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMerger.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.manager.BulletinMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.BulletinEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class ClearBulletinsEndpointMerger extends AbstractSingleEntityEndpoint<ClearBulletinsResultEntity> {
+    public static final Pattern CLEAR_BULLETINS_URI_PATTERN =
+            Pattern.compile("/nifi-api/(?:processors|controller-services|reporting-tasks|parameter-providers|registry-clients|flow-analysis-rules|remote-process-groups)/[a-f0-9\\-]{36}/bulletins");
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        return "POST".equalsIgnoreCase(method) && CLEAR_BULLETINS_URI_PATTERN.matcher(uri.getPath()).matches();
+    }
+
+    @Override
+    protected Class<ClearBulletinsResultEntity> getEntityClass() {
+        return ClearBulletinsResultEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(ClearBulletinsResultEntity clientEntity, Map<NodeIdentifier, ClearBulletinsResultEntity> entityMap,
+                                  Set<NodeResponse> successfulResponses, Set<NodeResponse> problematicResponses) {
+
+        // Sum up the total bulletins cleared across all nodes
+        int totalBulletinsCleared = 0;
+        for (final ClearBulletinsResultEntity entity : entityMap.values()) {
+            totalBulletinsCleared += entity.getBulletinsCleared();
+        }
+        clientEntity.setBulletinsCleared(totalBulletinsCleared);
+
+        // Merge the remaining bulletins from all nodes
+        final Map<NodeIdentifier, List<BulletinEntity>> bulletinMap = new HashMap<>();
+        for (final Map.Entry<NodeIdentifier, ClearBulletinsResultEntity> entry : entityMap.entrySet()) {
+            final NodeIdentifier nodeIdentifier = entry.getKey();
+            final ClearBulletinsResultEntity entity = entry.getValue();
+            final String nodeAddress = nodeIdentifier.getApiAddress() + ":" + nodeIdentifier.getApiPort();
+
+            if (entity.getBulletins() != null) {
+                final List<BulletinEntity> nodeBulletins = new ArrayList<>();
+                for (final BulletinEntity bulletinEntity : entity.getBulletins()) {
+                    if (bulletinEntity.getNodeAddress() == null) {
+                        bulletinEntity.setNodeAddress(nodeAddress);
+                    }
+                    if (bulletinEntity.getCanRead() && bulletinEntity.getBulletin() != null && bulletinEntity.getBulletin().getNodeAddress() == null) {
+                        bulletinEntity.getBulletin().setNodeAddress(nodeAddress);
+                    }
+                    nodeBulletins.add(bulletinEntity);
+                }
+                bulletinMap.put(nodeIdentifier, nodeBulletins);
+            }
+        }
+
+        // Use the BulletinMerger to merge the bulletins properly
+        final List<BulletinEntity> mergedBulletins = BulletinMerger.mergeBulletins(bulletinMap, entityMap.size());
+        clientEntity.setBulletins(mergedBulletins);
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMerger.java b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMerger.java
new file mode 100644
index 0000000000..c25b181876
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMerger.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class ClearBulletinsForGroupEndpointMerger extends AbstractSingleEntityEndpoint<ClearBulletinsForGroupResultsEntity> {
+    public static final Pattern CLEAR_BULLETINS_FOR_GROUP_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/[a-f0-9\\-]{36}/bulletins");
+
+    @Override
+    public boolean canHandle(URI uri, String method) {
+        return "POST".equalsIgnoreCase(method) && CLEAR_BULLETINS_FOR_GROUP_URI_PATTERN.matcher(uri.getPath()).matches();
+    }
+
+    @Override
+    protected Class<ClearBulletinsForGroupResultsEntity> getEntityClass() {
+        return ClearBulletinsForGroupResultsEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(ClearBulletinsForGroupResultsEntity clientEntity, Map<NodeIdentifier, ClearBulletinsForGroupResultsEntity> entityMap,
+                                  Set<NodeResponse> successfulResponses, Set<NodeResponse> problematicResponses) {
+
+        // Sum up the total bulletins cleared across all nodes
+        int totalBulletinsCleared = 0;
+        for (final ClearBulletinsForGroupResultsEntity entity : entityMap.values()) {
+            totalBulletinsCleared += entity.getBulletinsCleared();
+        }
+        clientEntity.setBulletinsCleared(totalBulletinsCleared);
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMergerTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMergerTest.java
new file mode 100644
index 0000000000..fb9929b05b
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsEndpointMergerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.BulletinEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClearBulletinsEndpointMergerTest {
+
+    @Test
+    public void testCanHandle() {
+        final ClearBulletinsEndpointMerger merger = new ClearBulletinsEndpointMerger();
+
+        // Test valid URIs
+        assertTrue(merger.canHandle(URI.create("/nifi-api/processors/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/controller-services/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/reporting-tasks/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/parameter-providers/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/registry-clients/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/flow-analysis-rules/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertTrue(merger.canHandle(URI.create("/nifi-api/remote-process-groups/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+
+        // Test invalid URIs
+        assertFalse(merger.canHandle(URI.create("/nifi-api/processors/12345678-1234-1234-1234-123456789012/bulletins"), "GET"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/process-groups/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/processors/invalid-id/bulletins"), "POST"));
+    }
+
+    @Test
+    public void testMergeResponses() {
+        final ClearBulletinsEndpointMerger merger = new ClearBulletinsEndpointMerger();
+
+        // Create test entities
+        final ClearBulletinsResultEntity clientEntity = new ClearBulletinsResultEntity();
+        clientEntity.setComponentId("test-component");
+        clientEntity.setBulletinsCleared(5);
+        clientEntity.setBulletins(createTestBulletins(2));
+
+        final Map<NodeIdentifier, ClearBulletinsResultEntity> entityMap = new HashMap<>();
+
+        // Node 1
+        final NodeIdentifier node1 = new NodeIdentifier("node1", "localhost", 8080, "localhost", 8081, "localhost", 8082, 8083, false);
+        final ClearBulletinsResultEntity entity1 = new ClearBulletinsResultEntity();
+        entity1.setComponentId("test-component");
+        entity1.setBulletinsCleared(5);
+        entity1.setBulletins(createTestBulletins(2));
+        entityMap.put(node1, entity1);
+
+        // Node 2
+        final NodeIdentifier node2 = new NodeIdentifier("node2", "localhost", 8090, "localhost", 8091, "localhost", 8092, 8093, false);
+        final ClearBulletinsResultEntity entity2 = new ClearBulletinsResultEntity();
+        entity2.setComponentId("test-component");
+        entity2.setBulletinsCleared(3);
+        entity2.setBulletins(createTestBulletins(1));
+        entityMap.put(node2, entity2);
+
+        // Merge responses
+        merger.mergeResponses(clientEntity, entityMap, null, null);
+
+        // Verify merged results
+        assertEquals(8, clientEntity.getBulletinsCleared()); // 5 + 3
+        // Note: BulletinMerger may consolidate bulletins with same message, so we just verify we have bulletins
+        assertFalse(clientEntity.getBulletins().isEmpty());
+    }
+
+    private List<BulletinEntity> createTestBulletins(int count) {
+        final List<BulletinEntity> bulletins = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            final BulletinEntity bulletin = new BulletinEntity();
+            bulletin.setId((long) i);
+            bulletin.setCanRead(true);
+            bulletin.setTimestamp(new java.util.Date());
+            bulletin.setSourceId("test-source");
+            bulletin.setGroupId("test-group");
+
+            // Create the nested BulletinDTO
+            final org.apache.nifi.web.api.dto.BulletinDTO bulletinDto = new org.apache.nifi.web.api.dto.BulletinDTO();
+            bulletinDto.setMessage("Test bulletin " + i);
+            bulletinDto.setLevel("INFO");
+            bulletinDto.setSourceId("test-source");
+            bulletinDto.setTimestamp(new java.util.Date());
+            bulletin.setBulletin(bulletinDto);
+
+            bulletins.add(bulletin);
+        }
+        return bulletins;
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMergerTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMergerTest.java
new file mode 100644
index 0000000000..133f69c462
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/endpoints/ClearBulletinsForGroupEndpointMergerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClearBulletinsForGroupEndpointMergerTest {
+
+    @Test
+    public void testCanHandle() {
+        final ClearBulletinsForGroupEndpointMerger merger = new ClearBulletinsForGroupEndpointMerger();
+
+        // Test valid URIs
+        assertTrue(merger.canHandle(URI.create("/nifi-api/process-groups/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+
+        // Test invalid URIs
+        assertFalse(merger.canHandle(URI.create("/nifi-api/process-groups/12345678-1234-1234-1234-123456789012/bulletins"), "GET"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/processors/12345678-1234-1234-1234-123456789012/bulletins"), "POST"));
+        assertFalse(merger.canHandle(URI.create("/nifi-api/process-groups/invalid-id/bulletins"), "POST"));
+    }
+
+    @Test
+    public void testMergeResponses() {
+        final ClearBulletinsForGroupEndpointMerger merger = new ClearBulletinsForGroupEndpointMerger();
+
+        // Create test entities
+        final ClearBulletinsForGroupResultsEntity clientEntity = new ClearBulletinsForGroupResultsEntity();
+        clientEntity.setBulletinsCleared(10);
+
+        final Map<NodeIdentifier, ClearBulletinsForGroupResultsEntity> entityMap = new HashMap<>();
+
+        // Node 1
+        final NodeIdentifier node1 = new NodeIdentifier("node1", "localhost", 8080, "localhost", 8081, "localhost", 8082, 8083, false);
+        final ClearBulletinsForGroupResultsEntity entity1 = new ClearBulletinsForGroupResultsEntity();
+        entity1.setBulletinsCleared(10);
+        entityMap.put(node1, entity1);
+
+        // Node 2
+        final NodeIdentifier node2 = new NodeIdentifier("node2", "localhost", 8090, "localhost", 8091, "localhost", 8092, 8093, false);
+        final ClearBulletinsForGroupResultsEntity entity2 = new ClearBulletinsForGroupResultsEntity();
+        entity2.setBulletinsCleared(7);
+        entityMap.put(node2, entity2);
+
+        // Merge responses
+        merger.mergeResponses(clientEntity, entityMap, null, null);
+
+        // Verify merged results
+        assertEquals(17, clientEntity.getBulletinsCleared()); // 10 + 7
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
index cd9e750b4c..7ea59751e8 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/events/VolatileBulletinRepository.java
@@ -23,6 +23,7 @@ import org.apache.nifi.reporting.ComponentType;
 import org.apache.nifi.util.RingBuffer;
 import org.apache.nifi.util.RingBuffer.Filter;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -310,4 +311,52 @@ public class VolatileBulletinRepository implements BulletinRepository {
             }
         }
     }
+
+    @Override
+    public int clearBulletinsForComponent(final String sourceId, final Instant fromTimestamp) throws IllegalArgumentException {
+        if (sourceId == null) {
+            throw new IllegalArgumentException("Source ID cannot be null");
+        }
+
+        return clearBulletinsForComponents(Collections.singleton(sourceId), fromTimestamp);
+    }
+
+    @Override
+    public int clearBulletinsForComponents(Collection<String> sourceIds, Instant fromTimestamp) throws IllegalArgumentException {
+        if (sourceIds == null || sourceIds.isEmpty()) {
+            throw new IllegalArgumentException("Source IDs cannot be null or empty");
+        }
+
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp cannot be null");
+        }
+
+        int totalCleared = 0;
+
+        // Create filter to match bulletins for any of the given sourceIds and at or before fromTimestamp
+        final Filter<Bulletin> clearFilter = bulletin -> {
+            // Match any of the source IDs
+            if (!sourceIds.contains(bulletin.getSourceId())) {
+                return false;
+            }
+
+            // Clear bulletins that are older than or equal to the specified timestamp
+            // Convert bulletin timestamp (Date) to Instant for comparison
+            return bulletin.getTimestamp() != null && !bulletin.getTimestamp().toInstant().isAfter(fromTimestamp);
+        };
+
+        // Iterate through all bulletin stores to find and clear matching bulletins
+        for (final ConcurrentMap<String, RingBuffer<Bulletin>> componentMap : bulletinStoreMap.values()) {
+            for (final RingBuffer<Bulletin> ringBuffer : componentMap.values()) {
+                // Count how many bulletins would be cleared first
+                final int countToRemove = ringBuffer.countSelectedElements(clearFilter);
+                totalCleared += countToRemove;
+
+                // Remove the bulletins
+                ringBuffer.removeSelectedElements(clearFilter);
+            }
+        }
+
+        return totalCleared;
+    }
 }
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/events/VolatileBulletinRepositoryTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/events/VolatileBulletinRepositoryTest.java
new file mode 100644
index 0000000000..3925edccb6
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/events/VolatileBulletinRepositoryTest.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.events;
+
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.BulletinQuery;
+import org.apache.nifi.reporting.ComponentType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class VolatileBulletinRepositoryTest {
+
+    private VolatileBulletinRepository repository;
+
+    @BeforeEach
+    public void setUp() {
+        repository = new VolatileBulletinRepository();
+    }
+
+    @Test
+    public void testClearBulletinsForSingleComponent() {
+        // Setup - add some bulletins
+        final String componentId = "test-component-1";
+        final Instant baseTime = Instant.now();
+        final Instant clearTime = baseTime.plusSeconds(5); // 5 seconds later
+
+        // Add bulletins before, at, and after clear time
+        addBulletin(componentId, "Before clear", Date.from(baseTime.plusSeconds(1)));
+        addBulletin(componentId, "At clear time", Date.from(clearTime));
+        addBulletin(componentId, "After clear", Date.from(baseTime.plusSeconds(10)));
+        addBulletin("other-component", "Other component", Date.from(baseTime.plusSeconds(6)));
+
+        // Initial count should be 4
+        assertEquals(4, getBulletinCount());
+
+        // Clear bulletins older than or equal to clearTime
+        int cleared = repository.clearBulletinsForComponent(componentId, clearTime);
+
+        // Should have cleared 2 bulletins (before clear time + at clear time)
+        assertEquals(2, cleared);
+
+        // Should have 2 bulletins remaining (after clear + other component)
+        assertEquals(2, getBulletinCount());
+    }
+
+    @Test
+    public void testClearBulletinsForMultipleComponents() {
+        // Setup - add bulletins for multiple components
+        final Instant baseTime = Instant.now();
+        final Instant clearTime = baseTime.plusSeconds(5);
+
+        addBulletin("component-1", "Component 1 before", Date.from(baseTime.plusSeconds(1)));
+        addBulletin("component-1", "Component 1 after", Date.from(baseTime.plusSeconds(6)));
+        addBulletin("component-2", "Component 2 before", Date.from(baseTime.plusSeconds(2)));
+        addBulletin("component-2", "Component 2 after", Date.from(baseTime.plusSeconds(7)));
+        addBulletin("component-3", "Component 3 after", Date.from(baseTime.plusSeconds(8)));
+
+        assertEquals(5, getBulletinCount());
+
+        // Clear bulletins for components 1 and 2 older than clearTime
+        int cleared = repository.clearBulletinsForComponents(
+                Arrays.asList("component-1", "component-2"), clearTime);
+
+        // Should have cleared 2 bulletins (component-1 before + component-2 before)
+        assertEquals(2, cleared);
+
+        // Should have 3 bulletins remaining (component-1 after + component-2 after + component-3 after)
+        assertEquals(3, getBulletinCount());
+    }
+
+    @Test
+    public void testClearBulletinsWithNullSourceId() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            repository.clearBulletinsForComponent(null, Instant.now());
+        });
+    }
+
+    @Test
+    public void testClearBulletinsWithNullTimestamp() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            repository.clearBulletinsForComponent("test-component", null);
+        });
+    }
+
+    @Test
+    public void testClearBulletinsForComponentsWithNullCollection() {
+        // Should throw exception when null collection is provided
+        assertThrows(IllegalArgumentException.class, () -> {
+            repository.clearBulletinsForComponents(null, Instant.now());
+        });
+    }
+
+    @Test
+    public void testClearBulletinsForComponentsWithEmptyCollection() {
+        // Should throw exception when empty collection is provided
+        assertThrows(IllegalArgumentException.class, () -> {
+            repository.clearBulletinsForComponents(Collections.emptyList(), Instant.now());
+        });
+    }
+
+    @Test
+    public void testClearBulletinsForComponentsWithNullTimestamp() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            repository.clearBulletinsForComponents(Arrays.asList("component-1"), null);
+        });
+    }
+
+    @Test
+    public void testClearBulletinsExactTimestampMatch() {
+        final String componentId = "test-component";
+        final Instant exactTime = Instant.now();
+
+        // Add bulletin exactly at the clear time
+        addBulletin(componentId, "Exact time", Date.from(exactTime));
+        addBulletin(componentId, "Before", Date.from(exactTime.minusSeconds(1)));
+        addBulletin(componentId, "After", Date.from(exactTime.plusSeconds(1)));
+
+        assertEquals(3, getBulletinCount());
+
+        // Clear bulletins older than or equal to exact time
+        int cleared = repository.clearBulletinsForComponent(componentId, exactTime);
+
+        // Should clear bulletins at and before exact time
+        assertEquals(2, cleared);
+        assertEquals(1, getBulletinCount());
+    }
+
+    @Test
+    public void testClearBulletinsNonExistentComponent() {
+        // Add some bulletins
+        addBulletin("existing-component", "Test message", new Date());
+        assertEquals(1, getBulletinCount());
+
+        // Try to clear bulletins for non-existent component
+        int cleared = repository.clearBulletinsForComponent("non-existent", Instant.now());
+
+        // Should clear 0 bulletins
+        assertEquals(0, cleared);
+        assertEquals(1, getBulletinCount());
+    }
+
+    @Test
+    public void testClearBulletinsSingleComponentDelegatesToMultiple() {
+        // Verify that single component method delegates to multiple components method
+        final String componentId = "test-component";
+        final Instant clearTime = Instant.now();
+
+        addBulletin(componentId, "Test message", Date.from(clearTime.plusSeconds(1)));
+
+        // Clear using single component method
+        int clearedSingle = repository.clearBulletinsForComponent(componentId, clearTime);
+
+        // Add same bulletin again
+        addBulletin(componentId, "Test message", Date.from(clearTime.plusSeconds(1)));
+
+        // Clear using multiple components method with single component
+        int clearedMultiple = repository.clearBulletinsForComponents(
+                Collections.singletonList(componentId), clearTime);
+
+        // Results should be the same
+        assertEquals(clearedSingle, clearedMultiple);
+    }
+
+    private void addBulletin(String sourceId, String message, Date timestamp) {
+        TestBulletin bulletin = new TestBulletin(sourceId, message, timestamp);
+        repository.addBulletin(bulletin);
+    }
+
+    private static class TestBulletin extends Bulletin {
+        private final String sourceId;
+        private final String message;
+        private final Date timestamp;
+
+        public TestBulletin(String sourceId, String message, Date timestamp) {
+            super(System.nanoTime());
+            this.sourceId = sourceId;
+            this.message = message;
+            this.timestamp = timestamp;
+        }
+
+        @Override
+        public String getSourceId() {
+            return sourceId;
+        }
+
+        @Override
+        public String getMessage() {
+            return message;
+        }
+
+        @Override
+        public Date getTimestamp() {
+            return timestamp;
+        }
+
+        @Override
+        public String getCategory() {
+            return "Test";
+        }
+
+        @Override
+        public String getLevel() {
+            return "INFO";
+        }
+
+        @Override
+        public ComponentType getSourceType() {
+            return ComponentType.PROCESSOR;
+        }
+
+        @Override
+        public String getSourceName() {
+            return "Test Source";
+        }
+
+        @Override
+        public String getGroupId() {
+            return "test-group";
+        }
+
+        @Override
+        public String getGroupName() {
+            return "Test Group";
+        }
+
+        @Override
+        public String getGroupPath() {
+            return "/";
+        }
+
+        @Override
+        public String getNodeAddress() {
+            return "localhost";
+        }
+
+        @Override
+        public String getFlowFileUuid() {
+            return null;
+        }
+    }
+
+    private int getBulletinCount() {
+        BulletinQuery query = new BulletinQuery.Builder().build();
+        List<Bulletin> bulletins = repository.findBulletins(query);
+        return bulletins.size();
+    }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index ff78ad29f3..0cddedbfec 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -101,6 +101,8 @@ import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.AssetEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
 import org.apache.nifi.web.api.entity.ComponentValidationResultEntity;
 import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
@@ -161,6 +163,7 @@ import org.apache.nifi.web.api.request.FlowMetricsRegistry;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -3090,4 +3093,37 @@ public interface NiFiServiceFacade {
      */
     AssetEntity deleteAsset(String parameterContextId, String assetId);
 
+    // -----------------------------------------
+    // Bulletin methods
+    // -----------------------------------------
+
+    /**
+     * Clears bulletins for the specified component.
+     *
+     * @param componentId the component id
+     * @param fromTimestamp the timestamp from which to clear bulletins (inclusive), must not be null
+     * @return the clear bulletin result entity
+     */
+    ClearBulletinsResultEntity clearBulletinsForComponent(String componentId, Instant fromTimestamp);
+
+
+    /**
+     * Clears bulletins for the specified components.
+     *
+     * @param processGroupId the process group id
+     * @param fromTimestamp the timestamp from which to clear bulletins (inclusive), must not be null
+     * @param componentIds the component IDs for which to clear bulletins
+     * @return the results of clearing bulletins for each component
+     */
+    ClearBulletinsForGroupResultsEntity clearBulletinsForComponents(String processGroupId, Instant fromTimestamp, Set<String> componentIds);
+
+    /**
+     * Filters components within the specified process group using the provided function.
+     *
+     * @param groupId the id of the process group
+     * @param getComponents function that takes a ProcessGroup and returns a set of component IDs
+     * @return set of component IDs returned by the function
+     */
+    Set<String> filterComponents(String groupId, Function<ProcessGroup, Set<String>> getComponents);
+
 }
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 319f795985..97b43df0f8 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -18,7 +18,6 @@ package org.apache.nifi.web;
 
 import io.prometheus.client.CollectorRegistry;
 import jakarta.ws.rs.WebApplicationException;
-import jakarta.ws.rs.core.Response;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.Strings;
 import org.apache.nifi.action.Action;
@@ -327,6 +326,8 @@ import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.AssetEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ComponentValidationResultEntity;
 import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
@@ -430,6 +431,7 @@ import org.springframework.stereotype.Service;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -4099,6 +4101,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 case PROCESSOR -> authorizableLookup.getProcessor(sourceId).getAuthorizable();
                 case REPORTING_TASK -> authorizableLookup.getReportingTask(sourceId).getAuthorizable();
                 case FLOW_ANALYSIS_RULE -> authorizableLookup.getFlowAnalysisRule(sourceId).getAuthorizable();
+                case FLOW_REGISTRY_CLIENT -> authorizableLookup.getFlowRegistryClient(sourceId).getAuthorizable();
                 case PARAMETER_PROVIDER -> authorizableLookup.getParameterProvider(sourceId).getAuthorizable();
                 case CONTROLLER_SERVICE -> authorizableLookup.getControllerService(sourceId).getAuthorizable();
                 case FLOW_CONTROLLER -> controllerFacade;
@@ -4106,8 +4109,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 case OUTPUT_PORT -> authorizableLookup.getOutputPort(sourceId);
                 case REMOTE_PROCESS_GROUP -> authorizableLookup.getRemoteProcessGroup(sourceId);
                 case PROCESS_GROUP -> authorizableLookup.getProcessGroup(sourceId).getAuthorizable();
-                default ->
-                        throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this bulletin.").build());
             };
         } catch (final ResourceNotFoundException e) {
             // if the underlying component is gone, disallow
@@ -7424,4 +7425,53 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     public void setAssetManager(final AssetManager assetManager) {
         this.assetManager = assetManager;
     }
+
+    @Override
+    public ClearBulletinsResultEntity clearBulletinsForComponent(final String componentId, final Instant fromTimestamp) {
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified");
+        }
+
+        final int bulletinsCleared = bulletinRepository.clearBulletinsForComponent(componentId, fromTimestamp);
+
+        // Get the current bulletins for the component after clearing
+        final List<Bulletin> currentBulletins = bulletinRepository.findBulletinsForSource(componentId);
+        final List<BulletinEntity> bulletinEntities = currentBulletins.stream()
+                .map(bulletin -> entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, false), authorizeBulletin(bulletin)))
+                .collect(Collectors.toList());
+
+        // Create the response entity
+        final ClearBulletinsResultEntity entity = new ClearBulletinsResultEntity();
+        entity.setComponentId(componentId);
+        entity.setBulletinsCleared(bulletinsCleared);
+        entity.setBulletins(bulletinEntities);
+
+        return entity;
+    }
+
+
+    @Override
+    public ClearBulletinsForGroupResultsEntity clearBulletinsForComponents(final String processGroupId, final Instant fromTimestamp, final Set<String> componentIds) {
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified");
+        }
+        if (componentIds == null || componentIds.isEmpty()) {
+            throw new IllegalArgumentException("Component IDs must be specified");
+        }
+
+        // Clear bulletins for all components specified
+        final int totalBulletinsCleared = bulletinRepository.clearBulletinsForComponents(componentIds, fromTimestamp);
+
+        // Create the response entity
+        final ClearBulletinsForGroupResultsEntity entity = new ClearBulletinsForGroupResultsEntity();
+        entity.setBulletinsCleared(totalBulletinsCleared);
+
+        return entity;
+    }
+
+    @Override
+    public Set<String> filterComponents(final String groupId, final Function<ProcessGroup, Set<String>> getComponents) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        return getComponents.apply(processGroup);
+    }
 }
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index 0010826316..90d98197a3 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -84,6 +84,11 @@ import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
 import org.apache.nifi.web.api.dto.VerifyConfigRequestDTO;
 import org.apache.nifi.web.api.entity.BulletinEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+import org.apache.nifi.web.api.entity.ParameterProviderEntity;
+import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
+import org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
 import org.apache.nifi.web.api.entity.ClusterEntity;
 import org.apache.nifi.web.api.entity.ComponentHistoryEntity;
 import org.apache.nifi.web.api.entity.ComponentStateEntity;
@@ -91,10 +96,8 @@ import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
 import org.apache.nifi.web.api.entity.ControllerConfigurationEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.Entity;
-import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
 import org.apache.nifi.web.api.entity.FlowAnalysisRuleRunStatusEntity;
 import org.apache.nifi.web.api.entity.FlowAnalysisRulesEntity;
-import org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
 import org.apache.nifi.web.api.entity.FlowRegistryClientTypesEntity;
 import org.apache.nifi.web.api.entity.FlowRegistryClientsEntity;
 import org.apache.nifi.web.api.entity.HistoryEntity;
@@ -102,7 +105,6 @@ import org.apache.nifi.web.api.entity.NarDetailsEntity;
 import org.apache.nifi.web.api.entity.NarSummariesEntity;
 import org.apache.nifi.web.api.entity.NarSummaryEntity;
 import org.apache.nifi.web.api.entity.NodeEntity;
-import org.apache.nifi.web.api.entity.ParameterProviderEntity;
 import org.apache.nifi.web.api.entity.PropertyDescriptorEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.entity.VerifyConfigRequestEntity;
@@ -121,6 +123,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.util.Collections;
+import java.time.Instant;
 import java.util.Date;
 import java.util.List;
 import java.util.Set;
@@ -354,6 +357,70 @@ public class ControllerResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears the bulletins for the specified parameter provider.
+     *
+     * @param id                       The id of the parameter provider
+     * @param requestClearBulletinEntity A clearBulletinsRequestEntity
+     * @return A clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/parameter-providers/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a parameter provider",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /parameter-providers/{uuid}")
+            }
+    )
+    public Response clearParameterProviderBulletins(
+            @Parameter(description = "The parameter provider id.", required = true)
+            @PathParam("id") final String id,
+            @Parameter(description = "The request to clear bulletins.", required = true)
+            final ClearBulletinsRequestEntity requestClearBulletinEntity) {
+
+        // Verify the request
+        if (requestClearBulletinEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        final Instant fromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestClearBulletinEntity);
+        }
+
+        final ParameterProviderEntity requestParameterProviderEntity = new ParameterProviderEntity();
+        requestParameterProviderEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestParameterProviderEntity,
+                lookup -> {
+                    final Authorizable authorizable = lookup.getParameterProvider(id).getAuthorizable();
+                    authorizable.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (parameterProviderEntity) -> {
+                    final Instant execFromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(parameterProviderEntity.getId(), execFromTimestamp);
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     // ---------------
     // reporting tasks
     // ---------------
@@ -1423,7 +1490,70 @@ public class ControllerResource extends ApplicationResource {
         entity.setRequest(dto);
         return entity;
     }
-    //--
+
+    /**
+     * Clears the bulletins for the specified flow analysis rule.
+     *
+     * @param id                       The id of the flow analysis rule
+     * @param requestClearBulletinEntity A clearBulletinsRequestEntity
+     * @return A clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/flow-analysis-rules/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a flow analysis rule",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /flow-analysis-rules/{uuid}")
+            }
+    )
+    public Response clearFlowAnalysisRuleBulletins(
+            @Parameter(description = "The flow analysis rule id.", required = true)
+            @PathParam("id") final String id,
+            @Parameter(description = "The request to clear bulletins.", required = true)
+            final ClearBulletinsRequestEntity requestClearBulletinEntity) {
+
+        // Verify the request
+        if (requestClearBulletinEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        final Instant fromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestClearBulletinEntity);
+        }
+
+        final FlowAnalysisRuleEntity requestFlowAnalysisRuleEntity = new FlowAnalysisRuleEntity();
+        requestFlowAnalysisRuleEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestFlowAnalysisRuleEntity,
+                lookup -> {
+                    final ComponentAuthorizable authorizable = lookup.getFlowAnalysisRule(id);
+                    authorizable.getAuthorizable().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (flowAnalysisRuleEntity) -> {
+                    final Instant execFromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(flowAnalysisRuleEntity.getId(), execFromTimestamp);
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
 
     // ----------
     // registries
@@ -2109,6 +2239,70 @@ public class ControllerResource extends ApplicationResource {
         return generateOkResponse(entity).build();
     }
 
+    /**
+     * Clears the bulletins for the specified registry client.
+     *
+     * @param id                       The id of the registry client
+     * @param requestClearBulletinEntity A clearBulletinsRequestEntity
+     * @return A clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/registry-clients/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a registry client",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /controller/registry-clients/{uuid}")
+            }
+    )
+    public Response clearRegistryClientBulletins(
+            @Parameter(description = "The registry client id.", required = true)
+            @PathParam("id") final String id,
+            @Parameter(description = "The request to clear bulletins.", required = true)
+            final ClearBulletinsRequestEntity requestClearBulletinEntity) {
+
+        // Verify the request
+        if (requestClearBulletinEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        final Instant fromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestClearBulletinEntity);
+        }
+
+        final FlowRegistryClientEntity requestFlowRegistryClientEntity = new FlowRegistryClientEntity();
+        requestFlowRegistryClientEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestFlowRegistryClientEntity,
+                lookup -> {
+                    final Authorizable authorizable = lookup.getFlowRegistryClient(id).getAuthorizable();
+                    authorizable.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (flowRegistryClientEntity) -> {
+                    final Instant execFromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(flowRegistryClientEntity.getId(), execFromTimestamp);
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Retrieves the types of flow registry clients that this NiFi supports.
      *
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java
index 3e6fd20347..6dde123c46 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java
@@ -70,6 +70,8 @@ import org.apache.nifi.web.api.dto.ControllerServiceDTO;
 import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
 import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.VerifyConfigRequestDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.ComponentStateEntity;
 import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
@@ -86,6 +88,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
 import java.util.Collections;
+import java.time.Instant;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -404,6 +407,77 @@ public class ControllerServiceResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears bulletins for a controller service.
+     *
+     * @param id The id of the controller service
+     * @param clearBulletinsRequestEntity The clear bulletin request
+     * @return a clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a controller service",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /controller-services/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The controller service id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The clear bulletin request specifying the timestamp from which to clear bulletins.",
+                    required = true
+            ) final ClearBulletinsRequestEntity clearBulletinsRequestEntity) {
+
+        if (clearBulletinsRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        // Validate the request
+        if (clearBulletinsRequestEntity.getFromTimestamp() == null) {
+            throw new IllegalArgumentException("From timestamp must be specified in the clear bulletin request.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsRequestEntity);
+        }
+
+        final ControllerServiceEntity requestControllerServiceEntity = new ControllerServiceEntity();
+        requestControllerServiceEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestControllerServiceEntity,
+                lookup -> {
+                    final Authorizable controllerService = lookup.getControllerService(id).getAuthorizable();
+                    controllerService.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (controllerServiceEntity) -> {
+                    final Instant fromTimestamp = clearBulletinsRequestEntity.getFromTimestamp();
+
+                    // Clear bulletins for the controller service
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(controllerServiceEntity.getId(), fromTimestamp);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Retrieves the references of the specified controller service.
      *
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index 124687dd3d..e80ad7f118 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -32,6 +32,7 @@ import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
@@ -105,6 +106,8 @@ import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.AdditionalDetailsEntity;
 import org.apache.nifi.web.api.entity.BannerEntity;
 import org.apache.nifi.web.api.entity.BulletinBoardEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
 import org.apache.nifi.web.api.entity.ClusterSearchResultsEntity;
 import org.apache.nifi.web.api.entity.ClusterSummaryEntity;
 import org.apache.nifi.web.api.entity.ComponentHistoryEntity;
@@ -172,6 +175,7 @@ import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.URI;
 import java.text.Collator;
+import java.time.Instant;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -629,7 +633,7 @@ public class FlowResource extends ApplicationResource {
                     @SecurityRequirement(name = "Read - /flow")
             },
             description = "If the uiOnly query parameter is provided with a value of true, the returned entity may only contain fields that are necessary for rendering the NiFi User Interface. As " +
-             "such, " +
+                    "such, " +
                     "the selected fields may change at any time, even during incremental releases, without warning. As a result, this parameter should not be provided by any client other than the UI."
     )
     public Response getControllerServicesFromController(@QueryParam("uiOnly") @DefaultValue("false") final boolean uiOnly,
@@ -681,7 +685,7 @@ public class FlowResource extends ApplicationResource {
                     @SecurityRequirement(name = "Read - /flow")
             },
             description = "If the uiOnly query parameter is provided with a value of true, the returned entity may only contain fields that are necessary for rendering the NiFi User Interface. As " +
-             "such, " +
+                    "such, " +
                     "the selected fields may change at any time, even during incremental releases, without warning. As a result, this parameter should not be provided by any client other than the UI."
     )
     public Response getControllerServicesFromGroup(
@@ -1230,6 +1234,122 @@ public class FlowResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears bulletins for components in the specified Process Group.
+     *
+     * @param id The id of the process group.
+     * @param clearBulletinsForGroupRequestEntity A clearBulletinsRequestEntity.
+     * @return A clearBulletinsResultsEntity.
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("process-groups/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for components in the specified Process Group.",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsForGroupResultsEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Read - /flow"),
+                    @SecurityRequirement(name = "Write - /process-groups/{uuid} - For the process group"),
+                    @SecurityRequirement(name = "Write - /{component-type}/{uuid} - For every component having bulletins cleared")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The process group id.",
+                    required = true
+            )
+            @PathParam("id") String id,
+            @Parameter(
+                    description = "The request to clear bulletins. If the components in the request are not specified, all authorized components will be considered.",
+                    required = true
+            ) final ClearBulletinsForGroupRequestEntity clearBulletinsForGroupRequestEntity) {
+
+        if (clearBulletinsForGroupRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletins request must be specified.");
+        }
+
+        // ensure the same id is being used
+        if (!id.equals(clearBulletinsForGroupRequestEntity.getId())) {
+            throw new IllegalArgumentException(String.format("The process group id (%s) in the request body does "
+                    + "not equal the process group id of the requested resource (%s).", clearBulletinsForGroupRequestEntity.getId(), id));
+        }
+
+        final Instant fromTimestamp = clearBulletinsForGroupRequestEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("The from timestamp must be specified.");
+        }
+
+        // if the components are not specified, gather all authorized components
+        if (clearBulletinsForGroupRequestEntity.getComponents() == null) {
+            // get component IDs that the user has write access to
+            final Set<String> writableComponentIds = serviceFacade.filterComponents(id, group -> {
+                final Set<String> componentIds = new HashSet<>();
+
+                // find all processors with write permissions
+                group.findAllProcessors().stream()
+                        .filter(processor -> processor.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                        .forEach(processor -> componentIds.add(processor.getIdentifier()));
+
+                // find all input ports with write permissions
+                group.findAllInputPorts().stream()
+                        .filter(inputPort -> inputPort.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                        .forEach(inputPort -> componentIds.add(inputPort.getIdentifier()));
+
+                // find all output ports with write permissions
+                group.findAllOutputPorts().stream()
+                        .filter(outputPort -> outputPort.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                        .forEach(outputPort -> componentIds.add(outputPort.getIdentifier()));
+
+                // find all rpgs with write permissions
+                group.findAllRemoteProcessGroups().stream()
+                        .filter(remoteProcessGroup -> remoteProcessGroup.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                        .forEach(remoteProcessGroup -> componentIds.add(remoteProcessGroup.getIdentifier()));
+
+                return componentIds;
+            });
+
+            // set the components (no revisions needed)
+            clearBulletinsForGroupRequestEntity.setComponents(writableComponentIds);
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsForGroupRequestEntity);
+        }
+
+        final ProcessGroupEntity requestProcessGroupEntity = new ProcessGroupEntity();
+        requestProcessGroupEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestProcessGroupEntity,
+                lookup -> {
+                    authorizeFlow();
+
+                    // ensure access to every component being cleared
+                    final Set<String> requestComponentsToClear = clearBulletinsForGroupRequestEntity.getComponents();
+                    requestComponentsToClear.forEach(componentId -> {
+                        final Authorizable connectable = lookup.getLocalConnectable(componentId);
+                        connectable.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                    });
+                },
+                () -> { },
+                (processGroupEntity) -> {
+                    // clear the bulletins
+                    final ClearBulletinsForGroupResultsEntity entity = serviceFacade.clearBulletinsForComponents(
+                            processGroupEntity.getId(), fromTimestamp, clearBulletinsForGroupRequestEntity.getComponents());
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     // ------
     // search
     // ------
@@ -2571,7 +2691,7 @@ public class FlowResource extends ApplicationResource {
         authorizeFlow();
         FlowVersionLocation baseVersionLocation = new FlowVersionLocation(branchIdA, bucketIdA, flowIdA, versionA);
         FlowVersionLocation comparedVersionLocation = new FlowVersionLocation(branchIdB, bucketIdB, flowIdB, versionB);
-            final FlowComparisonEntity versionDifference = serviceFacade.getVersionDifference(registryId, baseVersionLocation, comparedVersionLocation);
+        final FlowComparisonEntity versionDifference = serviceFacade.getVersionDifference(registryId, baseVersionLocation, comparedVersionLocation);
         // Note: with the current implementation, this is deterministic. However, the internal data structure used in comparison is set, thus
         // later changes might cause discrepancies. Practical use of the endpoint usually remains within one "page" though.
         return generateOkResponse(limitDifferences(versionDifference, offset, limit))
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/InputPortResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/InputPortResource.java
index d56d77a2e4..c53c950c9f 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/InputPortResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/InputPortResource.java
@@ -28,6 +28,7 @@ import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
@@ -46,6 +47,8 @@ import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.PortDTO;
 import org.apache.nifi.web.api.dto.PositionDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.PortEntity;
 import org.apache.nifi.web.api.entity.PortRunStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
@@ -54,6 +57,7 @@ import org.apache.nifi.web.api.request.LongParameter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
+import java.time.Instant;
 import java.util.Set;
 
 /**
@@ -387,6 +391,72 @@ public class InputPortResource extends ApplicationResource {
         return dto;
     }
 
+    /**
+     * Clears the bulletins for the specified input port.
+     *
+     * @param id The id of the input port
+     * @param requestClearBulletinEntity A clearBulletinsRequestEntity
+     * @return A clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for an input port",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /input-ports/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(description = "The input port id.", required = true)
+            @PathParam("id") final String id,
+            @Parameter(description = "The request to clear bulletins.", required = true)
+            final ClearBulletinsRequestEntity requestClearBulletinEntity) {
+
+        // Verify the request
+        if (requestClearBulletinEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        final Instant fromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestClearBulletinEntity);
+        }
+
+        final PortEntity requestPortEntity = new PortEntity();
+        requestPortEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestPortEntity,
+                lookup -> {
+                    final Authorizable inputPort = lookup.getInputPort(id);
+                    inputPort.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (portEntity) -> {
+                    // clear the bulletins
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(portEntity.getId(), fromTimestamp);
+
+                    // generate the response
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     @Autowired
     public void setServiceFacade(NiFiServiceFacade serviceFacade) {
         this.serviceFacade = serviceFacade;
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OutputPortResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OutputPortResource.java
index 703ea13bcc..92c27f6e40 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OutputPortResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OutputPortResource.java
@@ -28,6 +28,7 @@ import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
@@ -46,6 +47,8 @@ import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.PortDTO;
 import org.apache.nifi.web.api.dto.PositionDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.PortEntity;
 import org.apache.nifi.web.api.entity.PortRunStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
@@ -54,6 +57,7 @@ import org.apache.nifi.web.api.request.LongParameter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
+import java.time.Instant;
 import java.util.Set;
 
 /**
@@ -388,6 +392,72 @@ public class OutputPortResource extends ApplicationResource {
         return dto;
     }
 
+    /**
+     * Clears the bulletins for the specified output port.
+     *
+     * @param id The id of the output port
+     * @param requestClearBulletinEntity A clearBulletinsRequestEntity
+     * @return A clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for an output port",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /output-ports/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(description = "The output port id.", required = true)
+            @PathParam("id") final String id,
+            @Parameter(description = "The request to clear bulletins.", required = true)
+            final ClearBulletinsRequestEntity requestClearBulletinEntity) {
+
+        // Verify the request
+        if (requestClearBulletinEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        final Instant fromTimestamp = requestClearBulletinEntity.getFromTimestamp();
+        if (fromTimestamp == null) {
+            throw new IllegalArgumentException("From timestamp must be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestClearBulletinEntity);
+        }
+
+        final PortEntity requestPortEntity = new PortEntity();
+        requestPortEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestPortEntity,
+                lookup -> {
+                    final Authorizable outputPort = lookup.getOutputPort(id);
+                    outputPort.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (portEntity) -> {
+                    // clear the bulletins
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(portEntity.getId(), fromTimestamp);
+
+                    // generate the response
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     @Autowired
     public void setServiceFacade(NiFiServiceFacade serviceFacade) {
         this.serviceFacade = serviceFacade;
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
index da78992a9b..f5b6d8109f 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
@@ -102,6 +102,8 @@ import org.apache.nifi.web.api.entity.PropertyDescriptorEntity;
 import org.apache.nifi.web.api.entity.VerifyConfigRequestEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
 import org.apache.nifi.web.api.request.LongParameter;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.util.ComponentLifecycle;
 import org.apache.nifi.web.util.LifecycleManagementException;
 import org.apache.nifi.web.util.ParameterUpdateManager;
@@ -113,6 +115,7 @@ import org.springframework.stereotype.Controller;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -498,6 +501,77 @@ public class ParameterProviderResource extends AbstractParameterResource {
         );
     }
 
+    /**
+     * Clears bulletins for a parameter provider.
+     *
+     * @param id The id of the parameter provider
+     * @param clearBulletinsRequestEntity The clear bulletin request
+     * @return a clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a parameter provider",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /parameter-providers/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The parameter provider id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The clear bulletin request specifying the timestamp from which to clear bulletins.",
+                    required = true
+            ) final ClearBulletinsRequestEntity clearBulletinsRequestEntity) {
+
+        if (clearBulletinsRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        // Validate the request
+        if (clearBulletinsRequestEntity.getFromTimestamp() == null) {
+            throw new IllegalArgumentException("From timestamp must be specified in the clear bulletin request.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsRequestEntity);
+        }
+
+        final ParameterProviderEntity requestParameterProviderEntity = new ParameterProviderEntity();
+        requestParameterProviderEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestParameterProviderEntity,
+                lookup -> {
+                    final Authorizable parameterProvider = lookup.getParameterProvider(id).getAuthorizable();
+                    parameterProvider.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (parameterProviderEntity) -> {
+                    final Instant fromTimestamp = clearBulletinsRequestEntity.getFromTimestamp();
+
+                    // Clear bulletins for the parameter provider
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(parameterProviderEntity.getId(), fromTimestamp);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Updates the specified a Parameter Provider.
      *
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessorResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessorResource.java
index 07d8103fe0..f26f0390fe 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessorResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessorResource.java
@@ -70,6 +70,8 @@ import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
 import org.apache.nifi.web.api.dto.ProcessorDTO;
 import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
 import org.apache.nifi.web.api.dto.VerifyConfigRequestDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.ComponentStateEntity;
 import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
 import org.apache.nifi.web.api.entity.ProcessorDiagnosticsEntity;
@@ -87,6 +89,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
 import java.util.Collections;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -547,6 +550,77 @@ public class ProcessorResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears bulletins for a processor.
+     *
+     * @param id The id of the processor
+     * @param clearBulletinsRequestEntity The clear bulletin request
+     * @return a clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a processor",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /processors/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The processor id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The clear bulletin request specifying the timestamp from which to clear bulletins.",
+                    required = true
+            ) final ClearBulletinsRequestEntity clearBulletinsRequestEntity) {
+
+        if (clearBulletinsRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        // Validate the request
+        if (clearBulletinsRequestEntity.getFromTimestamp() == null) {
+            throw new IllegalArgumentException("From timestamp must be specified in the clear bulletin request.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsRequestEntity);
+        }
+
+        final ProcessorEntity requestProcessorEntity = new ProcessorEntity();
+        requestProcessorEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestProcessorEntity,
+                lookup -> {
+                    final Authorizable processor = lookup.getProcessor(id).getAuthorizable();
+                    processor.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (processorEntity) -> {
+                    final Instant fromTimestamp = clearBulletinsRequestEntity.getFromTimestamp();
+
+                    // Clear bulletins for the processor
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(processorEntity.getId(), fromTimestamp);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
 
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/RemoteProcessGroupResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/RemoteProcessGroupResource.java
index 3aac0f11da..0ce031200d 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/RemoteProcessGroupResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/RemoteProcessGroupResource.java
@@ -28,6 +28,7 @@ import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
@@ -50,6 +51,8 @@ import org.apache.nifi.web.api.dto.PositionDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
 import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.ComponentStateEntity;
 import org.apache.nifi.web.api.entity.RemotePortRunStatusEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
@@ -61,6 +64,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
 import java.net.URI;
+import java.time.Instant;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -225,6 +229,77 @@ public class RemoteProcessGroupResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears bulletins for a remote process group.
+     *
+     * @param id The id of the remote process group
+     * @param clearBulletinsRequestEntity The clear bulletin request
+     * @return a clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a remote process group",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /remote-process-groups/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The remote process group id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The clear bulletin request.",
+                    required = true
+            ) final ClearBulletinsRequestEntity clearBulletinsRequestEntity) {
+
+        if (clearBulletinsRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        // Validate the request
+        if (clearBulletinsRequestEntity.getFromTimestamp() == null) {
+            throw new IllegalArgumentException("From timestamp must be specified in the clear bulletin request.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsRequestEntity);
+        }
+
+        final RemoteProcessGroupEntity requestRemoteProcessGroupEntity = new RemoteProcessGroupEntity();
+        requestRemoteProcessGroupEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestRemoteProcessGroupEntity,
+                lookup -> {
+                    final Authorizable remoteProcessGroup = lookup.getRemoteProcessGroup(id);
+                    remoteProcessGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (remoteProcessGroupEntity) -> {
+                    final Instant fromTimestamp = clearBulletinsRequestEntity.getFromTimestamp();
+
+                    // Clear bulletins for the remote process group
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(remoteProcessGroupEntity.getId(), fromTimestamp);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Updates the specified remote process group input port.
      *
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ReportingTaskResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ReportingTaskResource.java
index 7b98fa8a36..c139ded985 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ReportingTaskResource.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ReportingTaskResource.java
@@ -66,6 +66,8 @@ import org.apache.nifi.web.api.dto.ConfigurationAnalysisDTO;
 import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
 import org.apache.nifi.web.api.dto.VerifyConfigRequestDTO;
+import org.apache.nifi.web.api.entity.ClearBulletinsRequestEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
 import org.apache.nifi.web.api.entity.ComponentStateEntity;
 import org.apache.nifi.web.api.entity.ConfigurationAnalysisEntity;
 import org.apache.nifi.web.api.entity.PropertyDescriptorEntity;
@@ -80,6 +82,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
 import java.util.Collections;
+import java.time.Instant;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -389,6 +392,77 @@ public class ReportingTaskResource extends ApplicationResource {
         );
     }
 
+    /**
+     * Clears bulletins for a reporting task.
+     *
+     * @param id The id of the reporting task
+     * @param clearBulletinsRequestEntity The clear bulletin request
+     * @return a clearBulletinsResultEntity
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/bulletins/clear-requests")
+    @Operation(
+            summary = "Clears bulletins for a reporting task",
+            responses = {
+                    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ClearBulletinsResultEntity.class))),
+                    @ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
+            },
+            security = {
+                    @SecurityRequirement(name = "Write - /reporting-tasks/{uuid}")
+            }
+    )
+    public Response clearBulletins(
+            @Parameter(
+                    description = "The reporting task id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The clear bulletin request specifying the timestamp from which to clear bulletins.",
+                    required = true
+            ) final ClearBulletinsRequestEntity clearBulletinsRequestEntity) {
+
+        if (clearBulletinsRequestEntity == null) {
+            throw new IllegalArgumentException("Clear bulletin request must be specified.");
+        }
+
+        // Validate the request
+        if (clearBulletinsRequestEntity.getFromTimestamp() == null) {
+            throw new IllegalArgumentException("From timestamp must be specified in the clear bulletin request.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, clearBulletinsRequestEntity);
+        }
+
+        final ReportingTaskEntity requestReportingTaskEntity = new ReportingTaskEntity();
+        requestReportingTaskEntity.setId(id);
+
+        return withWriteLock(
+                serviceFacade,
+                requestReportingTaskEntity,
+                lookup -> {
+                    final Authorizable reportingTask = lookup.getReportingTask(id).getAuthorizable();
+                    reportingTask.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                () -> { },
+                (reportingTaskEntity) -> {
+                    final Instant fromTimestamp = clearBulletinsRequestEntity.getFromTimestamp();
+
+                    // Clear bulletins for the reporting task
+                    final ClearBulletinsResultEntity entity = serviceFacade.clearBulletinsForComponent(reportingTaskEntity.getId(), fromTimestamp);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Updates the specified a Reporting Task.
      *
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 684435065f..104d39f850 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -3519,6 +3519,7 @@ public final class DtoFactory {
        dto.setId(bulletin.getId());
        dto.setNodeAddress(bulletin.getNodeAddress());
        dto.setTimestamp(bulletin.getTimestamp());
+       dto.setTimestampIso(bulletin.getTimestamp() != null ? bulletin.getTimestamp().toInstant() : null);
        dto.setGroupId(bulletin.getGroupId());
        dto.setSourceId(bulletin.getSourceId());
        dto.setSourceName(bulletin.getSourceName());
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index d176ae3d5a..39176ca8b1 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -794,6 +794,7 @@ public final class EntityFactory {
             entity.setSourceId(dto.getSourceId());
             entity.setGroupId(dto.getGroupId());
             entity.setTimestamp(dto.getTimestamp());
+            entity.setTimestampIso(dto.getTimestampIso());
             entity.setNodeAddress(dto.getNodeAddress());
             entity.setCanRead(canRead);
             if (canRead) {
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
index 49941a7233..f068301db4 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
@@ -98,6 +98,8 @@ import org.apache.nifi.web.api.dto.action.HistoryQueryDTO;
 import org.apache.nifi.web.api.dto.status.StatusHistoryDTO;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsResultEntity;
+import org.apache.nifi.web.api.entity.ClearBulletinsForGroupResultsEntity;
 import org.apache.nifi.web.api.entity.CopyRequestEntity;
 import org.apache.nifi.web.api.entity.CopyResponseEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
@@ -123,11 +125,13 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.Iterator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -1098,6 +1102,16 @@ public class StandardNiFiServiceFacadeTest {
             bulletinList.add(bulletin);
         }
 
+        @Override
+        public List<Bulletin> findBulletinsForSource(String sourceId) {
+            List<Bulletin> ans = new ArrayList<>();
+            for (Bulletin b : bulletinList) {
+                if (sourceId.equals(b.getSourceId()))
+                    ans.add(b);
+            }
+            return ans;
+        }
+
         @Override
         public List<Bulletin> findBulletinsForGroupBySource(String groupId) {
             List<Bulletin> ans = new ArrayList<>();
@@ -1108,6 +1122,120 @@ public class StandardNiFiServiceFacadeTest {
             return ans;
         }
 
+        @Override
+        public int clearBulletinsForComponent(String sourceId, Instant fromTimestamp) {
+            int cleared = 0;
+            final Iterator<Bulletin> iterator = bulletinList.iterator();
+            while (iterator.hasNext()) {
+                final Bulletin bulletin = iterator.next();
+                if (sourceId.equals(bulletin.getSourceId())
+                        && bulletin.getTimestamp() != null
+                        && !bulletin.getTimestamp().toInstant().isAfter(fromTimestamp)) {
+                    iterator.remove();
+                    cleared++;
+                }
+            }
+            return cleared;
+        }
+
+        @Override
+        public int clearBulletinsForComponents(Collection<String> sourceIds, Instant fromTimestamp) {
+            if (sourceIds == null || sourceIds.isEmpty()) {
+                throw new IllegalArgumentException("Source ID cannot be null or empty");
+            }
+
+            int cleared = 0;
+            final Iterator<Bulletin> iterator = bulletinList.iterator();
+            while (iterator.hasNext()) {
+                final Bulletin bulletin = iterator.next();
+                if (sourceIds.contains(bulletin.getSourceId())
+                        && bulletin.getTimestamp() != null
+                        && !bulletin.getTimestamp().toInstant().isAfter(fromTimestamp)) {
+                    iterator.remove();
+                    cleared++;
+                }
+            }
+            return cleared;
+        }
+
+        public void addTestBulletin(String sourceId, String message, Instant timestamp) {
+            TestBulletin bulletin = new TestBulletin(sourceId, message, Date.from(timestamp));
+            bulletinList.add(bulletin);
+        }
+
+    }
+
+    private static class TestBulletin extends Bulletin {
+        private final String sourceId;
+        private final String message;
+        private final Date timestamp;
+
+        public TestBulletin(String sourceId, String message, Date timestamp) {
+            super(System.nanoTime());
+            this.sourceId = sourceId;
+            this.message = message;
+            this.timestamp = timestamp;
+        }
+
+        @Override
+        public String getSourceId() {
+            return sourceId;
+        }
+
+        @Override
+        public String getMessage() {
+            return message;
+        }
+
+        @Override
+        public Date getTimestamp() {
+            return timestamp;
+        }
+
+        @Override
+        public String getCategory() {
+            return "Test";
+        }
+
+        @Override
+        public String getLevel() {
+            return "INFO";
+        }
+
+        @Override
+        public ComponentType getSourceType() {
+            return ComponentType.PROCESSOR;
+        }
+
+        @Override
+        public String getSourceName() {
+            return "Test Component";
+        }
+
+        @Override
+        public String getGroupId() {
+            return "test-group";
+        }
+
+        @Override
+        public String getGroupName() {
+            return "Test Group";
+        }
+
+        @Override
+        public String getGroupPath() {
+            return "/";
+        }
+
+        @Override
+        public String getNodeAddress() {
+            return null;
+        }
+
+        @Override
+        public String getFlowFileUuid() {
+            return null;
+        }
     }
 
     @Test
@@ -1449,4 +1577,91 @@ public class StandardNiFiServiceFacadeTest {
         verify(controllerFacade, times(1)).resetAllCounters();
         verify(dtoFactory, times(1)).createCountersDto(any());
     }
+
+    @Test
+    public void testClearBulletinsForComponent() {
+        final String componentId = "test-component-123";
+        final Instant fromTimestamp = Instant.now();
+
+        MockTestBulletinRepository bulletinRepository = new MockTestBulletinRepository();
+
+        // Add some test bulletins with different timestamps
+        Instant beforeTime = fromTimestamp.minusSeconds(10);
+        Instant afterTime = fromTimestamp.plusSeconds(10);
+
+        bulletinRepository.addTestBulletin(componentId, "Before message", beforeTime);
+        bulletinRepository.addTestBulletin(componentId, "At time message", fromTimestamp);
+        bulletinRepository.addTestBulletin(componentId, "After message", afterTime);
+        bulletinRepository.addTestBulletin("other-component", "Other component message", afterTime);
+
+        serviceFacade.setBulletinRepository(bulletinRepository);
+
+        ClearBulletinsResultEntity result = serviceFacade.clearBulletinsForComponent(componentId, fromTimestamp);
+
+        assertNotNull(result);
+        assertEquals(componentId, result.getComponentId());
+        assertEquals(2, result.getBulletinsCleared()); // Should clear bulletins older than or equal to fromTimestamp
+        assertEquals(2, bulletinRepository.bulletinList.size()); // 2 bulletins should remain (after time + other component)
+    }
+
+    @Test
+    public void testClearBulletinsForComponents() {
+        final String processGroupId = "test-process-group-123";
+        final Instant fromTimestamp = Instant.now();
+        final Set<String> componentIds = Set.of("component-1", "component-2");
+
+        MockTestBulletinRepository bulletinRepository = new MockTestBulletinRepository();
+
+        // Add test bulletins for multiple components
+        Instant beforeTime = fromTimestamp.minusSeconds(10);
+        Instant afterTime = fromTimestamp.plusSeconds(10);
+
+        bulletinRepository.addTestBulletin("component-1", "Component 1 before", beforeTime);
+        bulletinRepository.addTestBulletin("component-1", "Component 1 after", afterTime);
+        bulletinRepository.addTestBulletin("component-2", "Component 2 after", afterTime);
+        bulletinRepository.addTestBulletin("component-3", "Component 3 after", afterTime);
+
+        serviceFacade.setBulletinRepository(bulletinRepository);
+
+        ClearBulletinsForGroupResultsEntity result = serviceFacade.clearBulletinsForComponents(
+                processGroupId, fromTimestamp, componentIds);
+
+        assertNotNull(result);
+        assertEquals(1, result.getBulletinsCleared()); // Should clear 1 bulletin (component-1 before only)
+        assertEquals(3, bulletinRepository.bulletinList.size()); // 3 bulletins should remain (all "after" bulletins)
+    }
+
+    @Test
+    public void testClearBulletinsForComponentWithZeroResult() {
+        final String componentId = "non-existent-component";
+        final Instant fromTimestamp = Instant.now();
+
+        MockTestBulletinRepository bulletinRepository = new MockTestBulletinRepository();
+
+        // Add a bulletin for a different component
+        bulletinRepository.addTestBulletin("other-component", "Other message", Instant.now());
+
+        serviceFacade.setBulletinRepository(bulletinRepository);
+
+        ClearBulletinsResultEntity result = serviceFacade.clearBulletinsForComponent(componentId, fromTimestamp);
+
+        assertNotNull(result);
+        assertEquals(componentId, result.getComponentId());
+        assertEquals(0, result.getBulletinsCleared());
+        assertEquals(1, bulletinRepository.bulletinList.size()); // Original bulletin should remain
+    }
+
+    @Test
+    public void testClearBulletinsForComponentsWithEmptySet() {
+        final String processGroupId = "test-process-group-123";
+        final Instant fromTimestamp = Instant.now();
+        final Set<String> emptyComponentIds = Collections.emptySet();
+
+        MockTestBulletinRepository bulletinRepository = new MockTestBulletinRepository();
+        serviceFacade.setBulletinRepository(bulletinRepository);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            serviceFacade.clearBulletinsForComponents(processGroupId, fromTimestamp, emptyComponentIds);
+        });
+    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.spec.ts
new file mode 100644
index 0000000000..bfd5f7d264
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.spec.ts
@@ -0,0 +1,441 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TestBed } from '@angular/core/testing';
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+import * as d3 from 'd3';
+
+import { CanvasActionsService } from './canvas-actions.service';
+import { CanvasUtils } from './canvas-utils.service';
+import { CanvasView } from './canvas-view.service';
+import { CopyPasteService } from './copy-paste.service';
+import { Client } from '../../../service/client.service';
+import { CanvasState } from '../state';
+import { flowFeatureKey } from '../state/flow';
+import * as fromFlow from '../state/flow/flow.reducer';
+import { transformFeatureKey } from '../state/transform';
+import * as fromTransform from '../state/transform/transform.reducer';
+import { controllerServicesFeatureKey } from '../state/controller-services';
+import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
+import { parameterFeatureKey } from '../state/parameter';
+import * as fromParameter from '../state/parameter/parameter.reducer';
+import { queueFeatureKey } from '../../queue/state';
+import * as fromQueue from '../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../state/flow-analysis';
+import * as fromFlowAnalysis from '../state/flow-analysis/flow-analysis.reducer';
+import * as FlowActions from '../state/flow/flow.actions';
+import { ComponentType, NiFiCommon } from '@nifi/shared';
+
+// Mock d3 module
+jest.mock('d3', () => ({
+    selectAll: jest.fn()
+}));
+
+describe('CanvasActionsService', () => {
+    let service: CanvasActionsService;
+    let store: MockStore;
+    let mockNiFiCommon: jest.Mocked<NiFiCommon>;
+
+    const initialState: CanvasState = {
+        [flowFeatureKey]: fromFlow.initialState,
+        [transformFeatureKey]: fromTransform.initialState,
+        [controllerServicesFeatureKey]: fromControllerServices.initialState,
+        [parameterFeatureKey]: fromParameter.initialState,
+        [queueFeatureKey]: fromQueue.initialState,
+        [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
+    };
+
+    beforeEach(() => {
+        // Create mock NiFiCommon - only mock the methods we actually use in tests
+        mockNiFiCommon = {
+            getMostRecentBulletinTimestamp: jest.fn()
+        } as any;
+
+        TestBed.configureTestingModule({
+            providers: [
+                CanvasActionsService,
+                provideMockStore({ initialState }),
+                {
+                    provide: NiFiCommon,
+                    useValue: mockNiFiCommon
+                },
+                {
+                    provide: Router,
+                    useValue: {
+                        navigate: jest.fn()
+                    }
+                },
+                {
+                    provide: CanvasUtils,
+                    useValue: {
+                        isProcessor: jest.fn().mockReturnValue(false),
+                        isRemoteProcessGroup: jest.fn().mockReturnValue(false),
+                        isLabel: jest.fn().mockReturnValue(false),
+                        isConnection: jest.fn().mockReturnValue(false),
+                        canRead: jest.fn().mockReturnValue(true),
+                        canWrite: jest.fn().mockReturnValue(true),
+                        canCopy: jest.fn().mockReturnValue(true),
+                        canModify: jest.fn().mockReturnValue(true),
+                        canDelete: jest.fn().mockReturnValue(true),
+                        getComponentByType: jest.fn(),
+                        moveToFront: jest.fn(),
+                        moveToBack: jest.fn(),
+                        getSelectionByComponentType: jest.fn()
+                    }
+                },
+                {
+                    provide: CanvasView,
+                    useValue: {
+                        updateCanvasVisibility: jest.fn(),
+                        centerBoundingBox: jest.fn(),
+                        isCanvasInitialized: jest.fn().mockReturnValue(false)
+                    }
+                },
+                {
+                    provide: MatDialog,
+                    useValue: {
+                        open: jest.fn()
+                    }
+                },
+                {
+                    provide: Client,
+                    useValue: {
+                        isSecure: jest.fn().mockReturnValue(false)
+                    }
+                },
+                {
+                    provide: CopyPasteService,
+                    useValue: {
+                        isCopiedContentInView: jest.fn(),
+                        toOffsetPasteRequest: jest.fn(),
+                        toCenteredPasteRequest: jest.fn(),
+                        paste: jest.fn()
+                    }
+                }
+            ]
+        });
+        service = TestBed.inject(CanvasActionsService);
+        store = TestBed.inject(MockStore);
+
+        jest.spyOn(store, 'dispatch');
+    });
+
+    describe('clearBulletins action', () => {
+        let mockProcessGroupId: string;
+        let clearBulletinsAction: any;
+
+        beforeEach(() => {
+            mockProcessGroupId = 'test-process-group-id';
+            jest.spyOn(service, 'currentProcessGroupId').mockReturnValue(mockProcessGroupId);
+            clearBulletinsAction = service.getAction('clearBulletins');
+        });
+
+        describe('condition', () => {
+            let clearBulletinsCondition: any;
+            let mockD3SelectAll: jest.MockedFunction<typeof d3.selectAll>;
+
+            beforeEach(() => {
+                clearBulletinsCondition = service.getConditionFunction('clearBulletins');
+                mockD3SelectAll = d3.selectAll as jest.MockedFunction<typeof d3.selectAll>;
+            });
+
+            afterEach(() => {
+                jest.clearAllMocks();
+            });
+
+            it('should return true when selection is empty and components with bulletins exist', () => {
+                // Create a mock empty selection
+                const mockEmptySelection = {
+                    empty: jest.fn().mockReturnValue(true)
+                };
+
+                // Mock d3.selectAll to return non-empty selection for components with bulletins
+                mockD3SelectAll.mockReturnValue({
+                    empty: jest.fn().mockReturnValue(false)
+                } as any);
+
+                const result = clearBulletinsCondition(mockEmptySelection);
+
+                expect(result).toBe(true);
+                expect(mockD3SelectAll).toHaveBeenCalledWith('g.component.has-bulletins');
+            });
+
+            it('should return false when selection is empty and no components with bulletins exist', () => {
+                const mockEmptySelection = {
+                    empty: jest.fn().mockReturnValue(true)
+                };
+
+                // Mock d3.selectAll to return empty selection (no components with bulletins)
+                mockD3SelectAll.mockReturnValue({
+                    empty: jest.fn().mockReturnValue(true)
+                } as any);
+
+                const result = clearBulletinsCondition(mockEmptySelection);
+
+                expect(result).toBe(false);
+            });
+
+            it('should return true for single component with bulletins and write permissions', () => {
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue({
+                        type: ComponentType.Processor,
+                        permissions: { canWrite: true }
+                    }),
+                    classed: jest.fn().mockReturnValue(true)
+                };
+
+                const result = clearBulletinsCondition(mockSelection);
+
+                expect(result).toBe(true);
+                expect(mockSelection.classed).toHaveBeenCalledWith('has-bulletins');
+            });
+
+            it('should return false for single component without write permissions', () => {
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue({
+                        type: ComponentType.Processor,
+                        permissions: { canWrite: false }
+                    }),
+                    classed: jest.fn().mockReturnValue(true)
+                };
+
+                const result = clearBulletinsCondition(mockSelection);
+
+                expect(result).toBe(false);
+            });
+
+            it('should return true for process group with bulletins (permissions not required)', () => {
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue({
+                        type: ComponentType.ProcessGroup
+                    }),
+                    classed: jest.fn().mockReturnValue(true)
+                };
+
+                const result = clearBulletinsCondition(mockSelection);
+
+                expect(result).toBe(true);
+                expect(mockSelection.classed).toHaveBeenCalledWith('has-bulletins');
+            });
+
+            it('should return false for single component without bulletins', () => {
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue({
+                        type: ComponentType.Processor,
+                        permissions: { canWrite: true }
+                    }),
+                    classed: jest.fn().mockReturnValue(false)
+                };
+
+                const result = clearBulletinsCondition(mockSelection);
+
+                expect(result).toBe(false);
+            });
+
+            it('should return false for multiple component selection', () => {
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(2)
+                };
+
+                const result = clearBulletinsCondition(mockSelection);
+
+                expect(result).toBe(false);
+            });
+        });
+
+        describe('action', () => {
+            let mockD3SelectAll: jest.SpyInstance;
+
+            beforeEach(() => {
+                mockD3SelectAll = jest.spyOn(d3, 'selectAll');
+            });
+
+            it('should dispatch clearBulletinsForProcessGroup for empty selection when bulletins exist', () => {
+                const mockTimestamp = '2023-10-07T12:00:00.000Z';
+                const mockEmptySelection = {
+                    empty: jest.fn().mockReturnValue(true)
+                };
+
+                // Mock d3.selectAll to return components with bulletins
+                const mockEach = jest.fn((callback: (d: any) => void) => {
+                    // Simulate calling the callback with mock data that has bulletins
+                    callback({
+                        bulletins: [
+                            { timestampIso: '2023-10-07T12:00:00.000Z', bulletin: { timestamp: '12:00:00 UTC' } }
+                        ]
+                    });
+                });
+
+                mockD3SelectAll.mockReturnValue({
+                    each: mockEach
+                } as any);
+
+                // Mock getMostRecentBulletinTimestamp to return a timestamp
+                mockNiFiCommon.getMostRecentBulletinTimestamp.mockReturnValue(mockTimestamp);
+
+                clearBulletinsAction.action(mockEmptySelection);
+
+                expect(mockD3SelectAll).toHaveBeenCalledWith('g.component.has-bulletins');
+                expect(mockNiFiCommon.getMostRecentBulletinTimestamp).toHaveBeenCalled();
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    FlowActions.clearBulletinsForProcessGroup({
+                        request: {
+                            processGroupId: mockProcessGroupId,
+                            fromTimestamp: mockTimestamp
+                        }
+                    })
+                );
+            });
+
+            it('should not dispatch for empty selection if no bulletins are found', () => {
+                const mockEmptySelection = {
+                    empty: jest.fn().mockReturnValue(true)
+                };
+
+                // Mock d3.selectAll to return components without bulletins
+                const mockEach = jest.fn((callback: (d: any) => void) => {
+                    // Simulate calling the callback with mock data that has no bulletins
+                    callback({ bulletins: null });
+                });
+
+                mockD3SelectAll.mockReturnValue({
+                    each: mockEach
+                } as any);
+
+                // Mock getMostRecentBulletinTimestamp to return null (no bulletins)
+                mockNiFiCommon.getMostRecentBulletinTimestamp.mockReturnValue(null);
+
+                clearBulletinsAction.action(mockEmptySelection);
+
+                expect(store.dispatch).not.toHaveBeenCalled();
+            });
+
+            it('should dispatch clearBulletinsForProcessGroup for process group selection using getMostRecentBulletinTimestamp', () => {
+                const mockTimestamp = '2023-10-07T12:00:00.000Z';
+                const mockBulletins = [
+                    { timestampIso: '2023-10-07T12:00:00.000Z', bulletin: { timestamp: '12:00:00 UTC' } }
+                ];
+                const mockProcessGroupData = {
+                    type: ComponentType.ProcessGroup,
+                    id: 'selected-process-group-id',
+                    bulletins: mockBulletins
+                };
+
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue(mockProcessGroupData)
+                };
+
+                mockNiFiCommon.getMostRecentBulletinTimestamp.mockReturnValue(mockTimestamp);
+
+                clearBulletinsAction.action(mockSelection);
+
+                expect(mockNiFiCommon.getMostRecentBulletinTimestamp).toHaveBeenCalledWith(mockBulletins);
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    FlowActions.clearBulletinsForProcessGroup({
+                        request: {
+                            processGroupId: mockProcessGroupData.id,
+                            fromTimestamp: mockTimestamp
+                        }
+                    })
+                );
+            });
+
+            it('should dispatch clearBulletinForComponent for processor selection using getMostRecentBulletinTimestamp', () => {
+                const mockTimestamp = '2023-10-07T12:00:00.000Z';
+                const mockBulletins = [
+                    { timestampIso: '2023-10-07T12:00:00.000Z', bulletin: { timestamp: '12:00:00 UTC' } }
+                ];
+                const mockProcessorData = {
+                    type: ComponentType.Processor,
+                    id: 'processor-id',
+                    uri: 'processor-uri',
+                    bulletins: mockBulletins
+                };
+
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue(mockProcessorData)
+                };
+
+                mockNiFiCommon.getMostRecentBulletinTimestamp.mockReturnValue(mockTimestamp);
+
+                clearBulletinsAction.action(mockSelection);
+
+                expect(mockNiFiCommon.getMostRecentBulletinTimestamp).toHaveBeenCalledWith(mockBulletins);
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    FlowActions.clearBulletinsForComponent({
+                        request: {
+                            uri: mockProcessorData.uri,
+                            fromTimestamp: mockTimestamp,
+                            componentId: mockProcessorData.id,
+                            componentType: mockProcessorData.type
+                        }
+                    })
+                );
+            });
+
+            it('should dispatch clearBulletinForComponent for input port selection using getMostRecentBulletinTimestamp', () => {
+                const mockTimestamp = '2023-10-07T12:00:00.000Z';
+                const mockBulletins = [
+                    { timestampIso: '2023-10-07T12:00:00.000Z', bulletin: { timestamp: '12:00:00 UTC' } }
+                ];
+                const mockInputPortData = {
+                    type: ComponentType.InputPort,
+                    id: 'input-port-id',
+                    uri: 'input-port-uri',
+                    bulletins: mockBulletins
+                };
+
+                const mockSelection = {
+                    empty: jest.fn().mockReturnValue(false),
+                    size: jest.fn().mockReturnValue(1),
+                    datum: jest.fn().mockReturnValue(mockInputPortData)
+                };
+
+                mockNiFiCommon.getMostRecentBulletinTimestamp.mockReturnValue(mockTimestamp);
+
+                clearBulletinsAction.action(mockSelection);
+
+                expect(mockNiFiCommon.getMostRecentBulletinTimestamp).toHaveBeenCalledWith(mockBulletins);
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    FlowActions.clearBulletinsForComponent({
+                        request: {
+                            uri: mockInputPortData.uri,
+                            fromTimestamp: mockTimestamp,
+                            componentId: mockInputPortData.id,
+                            componentType: mockInputPortData.type
+                        }
+                    })
+                );
+            });
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.ts
index 370a387b0f..b335dccb55 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-actions.service.ts
@@ -18,6 +18,8 @@
 import { Injectable, inject } from '@angular/core';
 import { CanvasUtils } from './canvas-utils.service';
 import {
+    clearBulletinsForComponent,
+    clearBulletinsForProcessGroup,
     copySuccess,
     deleteComponents,
     disableComponents,
@@ -52,7 +54,7 @@ import { CanvasState } from '../state';
 import * as d3 from 'd3';
 import { MatDialog } from '@angular/material/dialog';
 import { CanvasView } from './canvas-view.service';
-import { ComponentType } from '@nifi/shared';
+import { ComponentType, NiFiCommon } from '@nifi/shared';
 import { Client } from '../../../service/client.service';
 import { CopyRequestContext, CopyRequestEntity, CopyResponseEntity } from '../../../state/copy';
 import { CopyPasteService } from './copy-paste.service';
@@ -83,6 +85,7 @@ export class CanvasActionsService {
     private dialog = inject(MatDialog);
     private client = inject(Client);
     private copyService = inject(CopyPasteService);
+    private nifiCommon = inject(NiFiCommon);
 
     private _actions: CanvasActions = {
         delete: {
@@ -535,6 +538,102 @@ export class CanvasActionsService {
                     })
                 );
             }
+        },
+        clearBulletins: {
+            id: 'clearBulletins',
+            condition: (selection: d3.Selection<any, any, any, any>) => {
+                // Empty selection means user is opening context menu for current process group
+                if (selection.empty()) {
+                    // Use d3 to select components with has-bulletins class
+                    // If this selection is non-empty, clear bulletins action should be available
+                    const componentsWithBulletins = d3.selectAll('g.component.has-bulletins');
+                    return !componentsWithBulletins.empty();
+                }
+
+                // Show clear bulletins option only when there are bulletins to clear
+                if (selection.size() === 1) {
+                    const d = selection.datum();
+
+                    // For individual components (not process groups), check write permissions
+                    if (d.type !== ComponentType.ProcessGroup) {
+                        if (!d.permissions.canWrite) {
+                            return false;
+                        }
+                    }
+
+                    // Check if the component has the has-bulletins class
+                    return selection.classed('has-bulletins');
+                }
+                return false;
+            },
+            action: (selection: d3.Selection<any, any, any, any>) => {
+                if (selection.empty()) {
+                    // Clear bulletins for current process group when no selection
+                    // Get all components with bulletins and find the most recent timestamp
+                    const componentsWithBulletins = d3.selectAll('g.component.has-bulletins');
+
+                    // Find the most recent timestamp across all components
+                    let mostRecentTimestamp: string | null = null;
+                    let mostRecentTime: number | null = null;
+                    componentsWithBulletins.each((d: any) => {
+                        if (d?.bulletins) {
+                            const componentTimestamp = this.nifiCommon.getMostRecentBulletinTimestamp(d.bulletins);
+                            if (componentTimestamp !== null) {
+                                const componentTime = new Date(componentTimestamp).getTime();
+                                if (mostRecentTime === null || componentTime > mostRecentTime) {
+                                    mostRecentTime = componentTime;
+                                    mostRecentTimestamp = componentTimestamp;
+                                }
+                            }
+                        }
+                    });
+
+                    if (mostRecentTimestamp === null || mostRecentTimestamp === undefined) {
+                        return; // no bulletins to clear
+                    }
+
+                    this.store.dispatch(
+                        clearBulletinsForProcessGroup({
+                            request: {
+                                processGroupId: this.currentProcessGroupId(),
+                                fromTimestamp: mostRecentTimestamp
+                            }
+                        })
+                    );
+                } else if (selection.size() === 1) {
+                    const d = selection.datum();
+
+                    // Get the most recent bulletin timestamp from the selected component
+                    const fromTimestamp = this.nifiCommon.getMostRecentBulletinTimestamp(d.bulletins);
+                    if (fromTimestamp === null) {
+                        return; // no bulletins to clear
+                    }
+
+                    if (d.type === ComponentType.ProcessGroup) {
+                        // For process groups, clear bulletins for all child components
+                        this.store.dispatch(
+                            clearBulletinsForProcessGroup({
+                                request: {
+                                    processGroupId: d.id,
+                                    fromTimestamp
+                                }
+                            })
+                        );
+                    } else {
+                        // Clear bulletins for this component
+                        this.store.dispatch(
+                            clearBulletinsForComponent({
+                                request: {
+                                    uri: d.uri,
+                                    fromTimestamp,
+                                    componentId: d.id,
+                                    componentType: d.type
+                                }
+                            })
+                        );
+                    }
+                }
+            }
         }
     };
 
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index 80bfeaaa7a..8fa55e42f5 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -1208,6 +1208,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
                 text: 'Change Color',
                 action: this.canvasActionsService.getActionFunction('changeColor')
             },
+            {
+                condition: this.canvasActionsService.getConditionFunction('clearBulletins'),
+                clazz: 'fa fa-eraser',
+                text: 'Clear Bulletins',
+                action: this.canvasActionsService.getActionFunction('clearBulletins')
+            },
             {
                 condition: (selection: d3.Selection<any, any, any, any>) => {
                     return this.canvasUtils.canRead(selection) && this.canvasUtils.isRemoteProcessGroup(selection);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
index 958abfe4b4..7ed80573fc 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
@@ -37,7 +37,7 @@ import { queueFeatureKey } from '../../queue/state';
 import * as fromQueue from '../state/queue/queue.reducer';
 import { flowAnalysisFeatureKey } from '../state/flow-analysis';
 import * as fromFlowAnalysis from '../state/flow-analysis/flow-analysis.reducer';
-import { ComponentType } from '@nifi/shared';
+import { ComponentType, BulletinEntity } from '@nifi/shared';
 import * as d3 from 'd3';
 
 describe('CanvasUtils', () => {
@@ -139,9 +139,9 @@ describe('CanvasUtils', () => {
         });
 
         it('should return false for multiple selections', () => {
-            const div1 = document.createElement('g');
-            const div2 = document.createElement('g');
-            const multiSelection = d3.selectAll([div1, div2]);
+            const g1 = document.createElement('g');
+            const g2 = document.createElement('g');
+            const multiSelection = d3.selectAll([g1, g2]);
             expect(service.isStoppable(multiSelection)).toBe(false);
         });
 
@@ -409,4 +409,254 @@ describe('CanvasUtils', () => {
             });
         });
     });
+
+    describe('bulletins', () => {
+        let mockSelection: any;
+
+        beforeEach(() => {
+            // Create a mock DOM element and selection
+            const g = document.createElement('g');
+            g.classList.add('component');
+
+            // Create mock bulletin background and icon elements
+            const bulletinBackground = document.createElement('rect');
+            bulletinBackground.classList.add('bulletin-background');
+            const bulletinIcon = document.createElement('text');
+            bulletinIcon.classList.add('bulletin-icon');
+
+            g.appendChild(bulletinBackground);
+            g.appendChild(bulletinIcon);
+
+            document.body.appendChild(g);
+
+            mockSelection = d3.select(g);
+        });
+
+        afterEach(() => {
+            // Clean up DOM
+            const elements = document.querySelectorAll('g.component');
+            elements.forEach((el) => el.remove());
+        });
+
+        it('should add has-bulletins class when bulletins are present', () => {
+            const bulletins: BulletinEntity[] = [
+                {
+                    id: 1,
+                    canRead: true,
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:00:00 UTC',
+                    bulletin: {
+                        id: 1,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'ERROR',
+                        message: 'Test error message',
+                        timestamp: '12:00:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                }
+            ];
+
+            service.bulletins(mockSelection, bulletins);
+
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+        });
+
+        it('should remove has-bulletins class when no bulletins are present', () => {
+            // First add the class
+            mockSelection.classed('has-bulletins', true);
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+
+            // Call bulletins with empty array
+            service.bulletins(mockSelection, []);
+
+            expect(mockSelection.classed('has-bulletins')).toBe(false);
+        });
+
+        it('should remove has-bulletins class when bulletins array is null', () => {
+            // First add the class
+            mockSelection.classed('has-bulletins', true);
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+
+            // Call bulletins with null
+            service.bulletins(mockSelection, null as any);
+
+            expect(mockSelection.classed('has-bulletins')).toBe(false);
+        });
+
+        it('should remove has-bulletins class when bulletins array is undefined', () => {
+            // First add the class
+            mockSelection.classed('has-bulletins', true);
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+
+            // Call bulletins with undefined
+            service.bulletins(mockSelection, undefined as any);
+
+            expect(mockSelection.classed('has-bulletins')).toBe(false);
+        });
+
+        it('should filter out bulletins with canRead=false and only show readable bulletins', () => {
+            const bulletins: BulletinEntity[] = [
+                {
+                    id: 1,
+                    canRead: false, // Not readable
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:00:00 UTC',
+                    bulletin: {
+                        id: 1,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'ERROR',
+                        message: 'Hidden error message',
+                        timestamp: '12:00:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                },
+                {
+                    id: 2,
+                    canRead: true, // Readable
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:01:00 UTC',
+                    bulletin: {
+                        id: 2,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'WARN',
+                        message: 'Visible warning message',
+                        timestamp: '12:00:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                }
+            ];
+
+            service.bulletins(mockSelection, bulletins);
+
+            // Should add has-bulletins class because there is one readable bulletin
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+        });
+
+        it('should remove has-bulletins class when all bulletins have canRead=false', () => {
+            const bulletins: BulletinEntity[] = [
+                {
+                    id: 1,
+                    canRead: false, // Not readable
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:00:00 UTC',
+                    bulletin: {
+                        id: 1,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'ERROR',
+                        message: 'Hidden error message',
+                        timestamp: '12:00:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                }
+            ];
+
+            service.bulletins(mockSelection, bulletins);
+
+            // Should not add has-bulletins class because there are no readable bulletins
+            expect(mockSelection.classed('has-bulletins')).toBe(false);
+        });
+
+        it('should remove has-bulletins class when bulletins have missing bulletin property', () => {
+            const bulletins: BulletinEntity[] = [
+                {
+                    id: 1,
+                    canRead: true,
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:00:00 UTC',
+                    bulletin: null as any // Missing bulletin data
+                }
+            ];
+
+            service.bulletins(mockSelection, bulletins);
+
+            // Should not add has-bulletins class because bulletin data is missing
+            expect(mockSelection.classed('has-bulletins')).toBe(false);
+        });
+
+        it('should set appropriate level class based on most severe bulletin', () => {
+            const bulletins: BulletinEntity[] = [
+                {
+                    id: 1,
+                    canRead: true,
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:00:00 UTC',
+                    bulletin: {
+                        id: 1,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'INFO',
+                        message: 'Info message',
+                        timestamp: '12:00:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                },
+                {
+                    id: 2,
+                    canRead: true,
+                    timestampIso: new Date().toISOString(),
+                    sourceId: 'source1',
+                    groupId: 'group1',
+                    timestamp: '12:01:00 UTC',
+                    bulletin: {
+                        id: 2,
+                        nodeAddress: 'localhost',
+                        category: 'test',
+                        groupId: 'group1',
+                        sourceId: 'source1',
+                        sourceName: 'Test Source',
+                        sourceType: 'COMPONENT',
+                        level: 'ERROR', // Most severe
+                        message: 'Error message',
+                        timestamp: '12:01:00 UTC',
+                        timestampIso: new Date().toISOString()
+                    }
+                }
+            ];
+
+            service.bulletins(mockSelection, bulletins);
+
+            expect(mockSelection.classed('has-bulletins')).toBe(true);
+
+            // Check that the error level class is applied to the bulletin icon
+            const bulletinIcon = mockSelection.select('text.bulletin-icon');
+            expect(bulletinIcon.classed('error')).toBe(true);
+            expect(bulletinIcon.classed('info')).toBe(false);
+        });
+    });
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index 47aa26ccf6..9131bbd113 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -1284,6 +1284,9 @@ export class CanvasUtils {
         selection.select('text.bulletin-icon').style('visibility', 'hidden');
         selection.select('rect.bulletin-background').style('visibility', 'hidden');
 
+        // remove the has-bulletins class
+        selection.classed('has-bulletins', false);
+
         // reset the canvas tooltip
         this.resetCanvasTooltip(selection);
     }
@@ -1316,6 +1319,9 @@ export class CanvasUtils {
                     .select('rect.bulletin-background')
                     .style('visibility', 'visible');
 
+                // add the has-bulletins class to indicate this component has bulletins
+                selection.classed('has-bulletins', true);
+
                 // reset any level-specifying classes that might have been there before
                 bulletinIcon
                     .classed('trace', false)
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/controller-service.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/controller-service.service.ts
index c5188a69e0..4e477aa948 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/controller-service.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/controller-service.service.ts
@@ -94,6 +94,13 @@ export class ControllerServiceService implements ControllerServiceCreator, Prope
         );
     }
 
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
+
     deleteControllerService(deleteControllerService: DeleteControllerServiceRequest): Observable<any> {
         const entity: ControllerServiceEntity = deleteControllerService.controllerService;
         const params = new HttpParams({
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow.service.ts
index 8a98836064..fae04753d0 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow.service.ts
@@ -19,6 +19,7 @@ import { Injectable, inject } from '@angular/core';
 import { Observable } from 'rxjs';
 import { HttpClient, HttpParams } from '@angular/common/http';
 import {
+    ClearBulletinsForGroupRequest,
     ComponentRunStatusRequest,
     ControllerServiceStateRequest,
     CreateComponentRequest,
@@ -55,7 +56,7 @@ import {
 import { Client } from '../../../service/client.service';
 import { ComponentType, NiFiCommon } from '@nifi/shared';
 import { ClusterConnectionService } from '../../../service/cluster-connection.service';
-import { PropertyDescriptorRetriever } from '../../../state/shared';
+import { ClearBulletinsRequest, PropertyDescriptorRetriever } from '../../../state/shared';
 
 @Injectable({ providedIn: 'root' })
 export class FlowService implements PropertyDescriptorRetriever {
@@ -475,4 +476,28 @@ export class FlowService implements PropertyDescriptorRetriever {
             'noreferrer'
         );
     }
+
+    /*
+        Clear Bulletins
+    */
+
+    clearBulletinForComponent(request: ClearBulletinsRequest): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
+
+    clearBulletinsForProcessGroup(request: ClearBulletinsForGroupRequest): Observable<any> {
+        const payload: any = {
+            id: request.processGroupId,
+            fromTimestamp: request.fromTimestamp
+        };
+
+        return this.httpClient.post(
+            `${FlowService.API}/flow/process-groups/${request.processGroupId}/bulletins/clear-requests`,
+            payload
+        );
+    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.actions.ts
index a966e1c571..1654b601c9 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.actions.ts
@@ -27,6 +27,8 @@ import {
     SelectControllerServiceRequest
 } from './index';
 import {
+    ClearBulletinsRequest,
+    ClearBulletinsResponse,
     CreateControllerServiceRequest,
     DisableControllerServiceDialogRequest,
     EditControllerServiceDialogRequest,
@@ -142,3 +144,13 @@ export const openChangeControllerServiceVersionDialog = createAction(
     `[Controller Services] Open Change Controller Service Version Dialog`,
     props<{ request: FetchComponentVersionsRequest }>()
 );
+
+export const clearControllerServiceBulletins = createAction(
+    '[Flow Designer Controller Services] Clear Controller Service Bulletins',
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearControllerServiceBulletinsSuccess = createAction(
+    '[Flow Designer Controller Services] Clear Controller Service Bulletins Success',
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
index f251b7c21e..1cb3f1fa2a 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,6 +23,15 @@ import { ReplaySubject, of, take, throwError } from 'rxjs';
 import { HttpErrorResponse } from '@angular/common/http';
 
 import * as ControllerServicesActions from './controller-services.actions';
+import {
+    clearControllerServiceBulletins,
+    clearControllerServiceBulletinsSuccess,
+    createControllerService,
+    createControllerServiceSuccess,
+    deleteControllerService,
+    deleteControllerServiceSuccess,
+    loadControllerServices
+} from './controller-services.actions';
 import { ControllerServicesEffects } from './controller-services.effects';
 import { ControllerServiceService } from '../../service/controller-service.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
@@ -32,9 +41,14 @@ import { PropertyTableHelperService } from '../../../../service/property-table-h
 import { ParameterHelperService } from '../../service/parameter-helper.service';
 import { ExtensionTypesService } from '../../../../service/extension-types.service';
 import { Client } from '../../../../service/client.service';
-import { Storage } from '@nifi/shared';
+import { ComponentType, Storage } from '@nifi/shared';
 import { ParameterContextService } from '../../../parameter-contexts/service/parameter-contexts.service';
+import { controllerServicesFeatureKey } from './index';
 import { initialState } from './controller-services.reducer';
+import { ClearBulletinsRequest, ClearBulletinsResponse } from '../../../../state/shared';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import { canvasFeatureKey } from '../index';
+import { flowFeatureKey } from '../flow';
 
 describe('ControllerServicesEffects', () => {
     interface SetupOptions {
@@ -54,36 +68,103 @@ describe('ControllerServicesEffects', () => {
     } as any;
 
     async function setup({ controllerServicesState = initialState }: SetupOptions = {}) {
+        const controllerServiceServiceSpy = {
+            getControllerServices: jest.fn(),
+            getControllerService: jest.fn(),
+            getFlow: jest.fn(),
+            createControllerService: jest.fn(),
+            updateControllerService: jest.fn(),
+            deleteControllerService: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<ControllerServiceService>;
+
         await TestBed.configureTestingModule({
             providers: [
                 ControllerServicesEffects,
                 provideMockActions(() => action$),
                 provideMockStore({
                     initialState: {
-                        canvas: {
-                            controllerServiceListing: controllerServicesState
+                        [canvasFeatureKey]: {
+                            [controllerServicesFeatureKey]: controllerServicesState,
+                            [flowFeatureKey]: {
+                                id: 'root'
+                            }
                         }
                     }
                 }),
                 {
                     provide: ControllerServiceService,
-                    useValue: { getControllerServices: jest.fn(), getFlow: jest.fn() }
+                    useValue: controllerServiceServiceSpy
+                },
+                {
+                    provide: ErrorHelper,
+                    useValue: {
+                        handleLoadingError: jest.fn(),
+                        getErrorString: jest.fn(),
+                        showErrorInContext: jest.fn(),
+                        fullScreenError: jest.fn()
+                    }
+                },
+                {
+                    provide: MatDialog,
+                    useValue: {
+                        open: jest.fn(),
+                        closeAll: jest.fn()
+                    }
+                },
+                {
+                    provide: Router,
+                    useValue: {
+                        navigate: jest.fn()
+                    }
+                },
+                {
+                    provide: PropertyTableHelperService,
+                    useValue: {
+                        getComponentHistory: jest.fn(),
+                        createNewProperty: jest.fn(),
+                        createNewService: jest.fn()
+                    }
+                },
+                {
+                    provide: ParameterHelperService,
+                    useValue: {
+                        convertToParameter: jest.fn()
+                    }
+                },
+                {
+                    provide: ExtensionTypesService,
+                    useValue: {
+                        getControllerServiceVersionsForType: jest.fn()
+                    }
                 },
-                { provide: ErrorHelper, useValue: { handleLoadingError: jest.fn(), getErrorString: jest.fn() } },
-                { provide: MatDialog, useValue: { open: jest.fn(), closeAll: jest.fn() } },
-                { provide: Router, useValue: { navigate: jest.fn() } },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: ParameterHelperService, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} },
-                { provide: Client, useValue: {} },
-                { provide: Storage, useValue: { setItem: jest.fn(), getItem: jest.fn() } },
-                { provide: ParameterContextService, useValue: { getParameterContext: jest.fn() } }
+                {
+                    provide: Client,
+                    useValue: {
+                        getClientId: jest.fn().mockReturnValue('test-client-id')
+                    }
+                },
+                {
+                    provide: Storage,
+                    useValue: {
+                        setItem: jest.fn(),
+                        getItem: jest.fn()
+                    }
+                },
+                {
+                    provide: ParameterContextService,
+                    useValue: {
+                        getParameterContext: jest.fn()
+                    }
+                }
             ]
         }).compileComponents();
 
         const store = TestBed.inject(MockStore);
         const effects = TestBed.inject(ControllerServicesEffects);
-        const controllerServiceService = TestBed.inject(ControllerServiceService);
+        const controllerServiceService = TestBed.inject(
+            ControllerServiceService
+        ) as jest.Mocked<ControllerServiceService>;
         const errorHelper = TestBed.inject(ErrorHelper);
 
         return { effects, store, controllerServiceService, errorHelper };
@@ -221,4 +302,344 @@ describe('ControllerServicesEffects', () => {
             expect(result).toEqual(errorAction);
         });
     });
+
+    describe('clearControllerServiceBulletins$', () => {
+        it('should dispatch clearControllerServiceBulletinsSuccess action when bulletins are cleared successfully', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                bulletinsCleared: 5,
+                bulletins: []
+            };
+
+            const expectedResponse: ClearBulletinsResponse = {
+                componentId: request.componentId,
+                bulletinsCleared: 5,
+                bulletins: [],
+                componentType: request.componentType
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(controllerServiceService.clearBulletins).toHaveBeenCalledWith({
+                uri: request.uri,
+                fromTimestamp: request.fromTimestamp
+            });
+            expect(result).toEqual(clearControllerServiceBulletinsSuccess({ response: expectedResponse }));
+        });
+
+        it('should handle bulletinsCleared being undefined and default to 0', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                // bulletinsCleared is undefined
+                bulletins: []
+            };
+
+            const expectedResponse: ClearBulletinsResponse = {
+                componentId: request.componentId,
+                bulletinsCleared: 0, // Should default to 0
+                bulletins: [],
+                componentType: request.componentType
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(clearControllerServiceBulletinsSuccess({ response: expectedResponse }));
+        });
+
+        it('should preserve request fields in the success response', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'different-test-uri',
+                fromTimestamp: '2023-06-15T08:30:00.000Z',
+                componentId: 'different-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                bulletinsCleared: 3,
+                bulletins: []
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            const result = await new Promise<any>((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result.response.componentId).toBe(request.componentId);
+            expect(result.response.componentType).toBe(request.componentType);
+            expect(result.response.bulletinsCleared).toBe(3);
+            expect(result.response.bulletins).toEqual([]);
+        });
+
+        it('should call service with correct parameters', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'test-controller-service-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                bulletinsCleared: 2,
+                bulletins: []
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            await new Promise<void>((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(() => {
+                    expect(controllerServiceService.clearBulletins).toHaveBeenCalledWith({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    });
+                    expect(controllerServiceService.clearBulletins).toHaveBeenCalledTimes(1);
+                    resolve();
+                })
+            );
+        });
+
+        it('should handle zero bulletins cleared', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                bulletinsCleared: 0,
+                bulletins: []
+            };
+
+            const expectedResponse: ClearBulletinsResponse = {
+                componentId: request.componentId,
+                bulletinsCleared: 0,
+                bulletins: [],
+                componentType: request.componentType
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(clearControllerServiceBulletinsSuccess({ response: expectedResponse }));
+        });
+
+        it('should handle large number of bulletins cleared', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request: ClearBulletinsRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-controller-service-id',
+                componentType: ComponentType.ControllerService
+            };
+
+            const serviceResponse = {
+                bulletinsCleared: 1000,
+                bulletins: []
+            };
+
+            const expectedResponse: ClearBulletinsResponse = {
+                componentId: request.componentId,
+                bulletinsCleared: 1000,
+                bulletins: [],
+                componentType: request.componentType
+            };
+
+            jest.spyOn(controllerServiceService, 'clearBulletins').mockReturnValue(of(serviceResponse));
+            action$.next(clearControllerServiceBulletins({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(clearControllerServiceBulletinsSuccess({ response: expectedResponse }));
+        });
+    });
+
+    describe('createControllerService$', () => {
+        it('should dispatch createControllerServiceSuccess when service is created successfully', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request = {
+                revision: { clientId: 'test-client', version: 0 },
+                processGroupId: 'test-process-group-id',
+                controllerServiceType: 'org.apache.nifi.TestService',
+                controllerServiceBundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' }
+            };
+            const serviceResponse = {
+                id: 'new-service-id',
+                component: { name: 'New Test Service' }
+            } as any;
+
+            jest.spyOn(controllerServiceService, 'createControllerService').mockReturnValue(of(serviceResponse));
+            action$.next(createControllerService({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.createControllerService$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(
+                createControllerServiceSuccess({
+                    response: { controllerService: serviceResponse }
+                })
+            );
+            expect(controllerServiceService.createControllerService).toHaveBeenCalledWith(request);
+        });
+
+        it('should dispatch snackBarError and close dialogs when creation fails', async () => {
+            const { effects, controllerServiceService, errorHelper } = await setup();
+
+            const request = {
+                revision: { clientId: 'test-client', version: 0 },
+                processGroupId: 'test-process-group-id',
+                controllerServiceType: 'org.apache.nifi.TestService',
+                controllerServiceBundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' }
+            };
+            const errorResponse = new HttpErrorResponse({
+                error: 'Creation failed',
+                status: 500,
+                statusText: 'Internal Server Error'
+            });
+
+            const mockDialog = TestBed.inject(MatDialog);
+            jest.spyOn(mockDialog, 'closeAll');
+            jest.spyOn(errorHelper as any, 'getErrorString').mockReturnValue('Creation failed');
+            jest.spyOn(controllerServiceService, 'createControllerService').mockReturnValue(
+                throwError(() => errorResponse)
+            );
+
+            action$.next(createControllerService({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.createControllerService$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(ErrorActions.snackBarError({ error: 'Creation failed' }));
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+        });
+    });
+
+    describe('deleteControllerService$', () => {
+        it('should dispatch deleteControllerServiceSuccess when service is deleted successfully', async () => {
+            const { effects, controllerServiceService } = await setup();
+
+            const request = {
+                controllerService: {
+                    id: 'service-to-delete',
+                    component: { name: 'Service to Delete' }
+                } as any
+            };
+            const serviceResponse = {
+                id: 'service-to-delete',
+                component: { name: 'Service to Delete' }
+            } as any;
+
+            jest.spyOn(controllerServiceService, 'deleteControllerService').mockReturnValue(of(serviceResponse));
+            action$.next(deleteControllerService({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.deleteControllerService$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(
+                deleteControllerServiceSuccess({
+                    response: { controllerService: serviceResponse }
+                })
+            );
+            expect(controllerServiceService.deleteControllerService).toHaveBeenCalledWith(request);
+        });
+
+        it('should dispatch snackBarError when deletion fails', async () => {
+            const { effects, controllerServiceService, errorHelper } = await setup();
+
+            const request = {
+                controllerService: {
+                    id: 'service-to-delete',
+                    component: { name: 'Service to Delete' }
+                } as any
+            };
+            const errorResponse = new HttpErrorResponse({
+                error: 'Deletion failed',
+                status: 500,
+                statusText: 'Internal Server Error'
+            });
+
+            jest.spyOn(errorHelper as any, 'getErrorString').mockReturnValue('Deletion failed');
+            jest.spyOn(controllerServiceService, 'deleteControllerService').mockReturnValue(
+                throwError(() => errorResponse)
+            );
+
+            action$.next(deleteControllerService({ request }));
+
+            const result = await new Promise((resolve) =>
+                effects.deleteControllerService$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(ErrorActions.snackBarError({ error: 'Deletion failed' }));
+        });
+    });
+
+    describe('deleteControllerServiceSuccess$', () => {
+        it('should dispatch loadControllerServices after successful deletion', async () => {
+            const { effects } = await setup();
+
+            const response = {
+                controllerService: {
+                    id: 'deleted-service',
+                    component: { name: 'Deleted Service' }
+                } as any
+            };
+
+            action$.next(deleteControllerServiceSuccess({ response }));
+
+            const result = await new Promise((resolve) =>
+                effects.deleteControllerServiceSuccess$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(
+                loadControllerServices({
+                    request: { processGroupId: 'root' }
+                })
+            );
+        });
+    });
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
index c8ca4aff37..08c075c205 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts
@@ -871,4 +871,30 @@ export class ControllerServicesEffects {
             return ['/settings', 'registry-clients', reference.id];
         }
     }
+
+    clearControllerServiceBulletins$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ControllerServicesActions.clearControllerServiceBulletins),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.controllerServiceService.clearBulletins({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    })
+                ).pipe(
+                    map((response) =>
+                        ControllerServicesActions.clearControllerServiceBulletinsSuccess({
+                            response: {
+                                componentId: request.componentId,
+                                bulletinsCleared: response.bulletinsCleared || 0,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    )
+                )
+            )
+        )
+    );
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.spec.ts
new file mode 100644
index 0000000000..f0d5fcaccd
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.spec.ts
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { controllerServicesReducer, initialState } from './controller-services.reducer';
+import { clearControllerServiceBulletinsSuccess } from './controller-services.actions';
+import { ControllerServicesState } from './index';
+import { ComponentType, BulletinEntity } from '@nifi/shared';
+
+describe('Controller Services Reducer', () => {
+    describe('clearControllerServiceBulletinsSuccess', () => {
+        let testState: ControllerServicesState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'controller-service-1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'controller-service-1',
+                    sourceName: 'Test Controller Service',
+                    sourceType: 'COMPONENT',
+                    level: 'ERROR',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialState,
+                controllerServices: [
+                    {
+                        id: 'controller-service-1',
+                        uri: 'test-uri-1',
+                        bulletins: [
+                            createTestBulletin(1, olderTimestamp), // Should be removed
+                            createTestBulletin(2, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'controller-service-1',
+                            name: 'Test Controller Service 1',
+                            type: 'org.apache.nifi.TestControllerService',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'ENABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            referencingComponents: [],
+                            comments: '',
+                            deprecated: false,
+                            extensionMissing: false,
+                            multipleVersionsAvailable: false,
+                            persistsState: false,
+                            restricted: false,
+                            validationStatus: 'VALID'
+                        },
+                        status: {
+                            runStatus: 'ENABLED',
+                            validationStatus: 'VALID'
+                        }
+                    } as any,
+                    {
+                        id: 'controller-service-2',
+                        uri: 'test-uri-2',
+                        bulletins: [
+                            createTestBulletin(3, olderTimestamp), // Should be removed
+                            createTestBulletin(4, testTimestamp), // Should be kept (equal to timestamp)
+                            createTestBulletin(5, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'controller-service-2',
+                            name: 'Test Controller Service 2',
+                            type: 'org.apache.nifi.TestControllerService',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'ENABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            referencingComponents: [],
+                            comments: '',
+                            deprecated: false,
+                            extensionMissing: false,
+                            multipleVersionsAvailable: false,
+                            persistsState: false,
+                            restricted: false,
+                            validationStatus: 'VALID'
+                        },
+                        status: {
+                            runStatus: 'ENABLED',
+                            validationStatus: 'VALID'
+                        }
+                    } as any,
+                    {
+                        id: 'controller-service-3',
+                        uri: 'test-uri-3',
+                        bulletins: [
+                            createTestBulletin(6, olderTimestamp) // Should be removed
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'controller-service-3',
+                            name: 'Test Controller Service 3',
+                            type: 'org.apache.nifi.TestControllerService',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'ENABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            referencingComponents: [],
+                            comments: '',
+                            deprecated: false,
+                            extensionMissing: false,
+                            multipleVersionsAvailable: false,
+                            persistsState: false,
+                            restricted: false,
+                            validationStatus: 'VALID'
+                        },
+                        status: {
+                            runStatus: 'ENABLED',
+                            validationStatus: 'VALID'
+                        }
+                    } as any
+                ]
+            };
+        });
+
+        it('should replace bulletins for controller service', () => {
+            // Server returns bulletins that remain after clearing (newer bulletins)
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const service = newState.controllerServices.find((s) => s.id === 'controller-service-1') as any;
+            expect(service.bulletins).toHaveLength(1);
+            expect(service.bulletins[0].id).toBe(2); // Only newer bulletin remains
+        });
+
+        it('should replace bulletins keeping equal and newer timestamps', () => {
+            // Server returns bulletins that remain after clearing (equal and newer bulletins)
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-2',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(4, testTimestamp), createTestBulletin(5, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const service = newState.controllerServices.find((s) => s.id === 'controller-service-2') as any;
+            expect(service.bulletins).toHaveLength(2);
+            expect(service.bulletins.map((b: any) => b.id)).toEqual([4, 5]); // Equal and newer bulletins remain
+        });
+
+        it('should remove all older bulletins when all are older than timestamp', () => {
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-3',
+                    bulletinsCleared: 1,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const service = newState.controllerServices.find((s) => s.id === 'controller-service-3') as any;
+            expect(service.bulletins).toHaveLength(0); // All bulletins removed
+        });
+
+        it('should not affect other controller services when updating specific service', () => {
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            // Other services should remain unchanged
+            const service2 = newState.controllerServices.find((s) => s.id === 'controller-service-2') as any;
+            expect(service2.bulletins).toHaveLength(3); // Original count unchanged
+
+            const service3 = newState.controllerServices.find((s) => s.id === 'controller-service-3') as any;
+            expect(service3.bulletins).toHaveLength(1); // Original count unchanged
+        });
+
+        it('should handle controller service with no bulletins', () => {
+            testState.controllerServices.push({
+                id: 'controller-service-no-bulletins',
+                uri: 'test-uri-no-bulletins',
+                bulletins: undefined,
+                revision: { version: 1, clientId: 'test' },
+                permissions: { canRead: true, canWrite: true },
+                component: {
+                    id: 'controller-service-no-bulletins',
+                    name: 'Test Controller Service No Bulletins',
+                    type: 'org.apache.nifi.TestControllerService',
+                    bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                    state: 'ENABLED',
+                    properties: {},
+                    descriptors: {},
+                    validationErrors: [],
+                    referencingComponents: [],
+                    comments: '',
+                    deprecated: false,
+                    extensionMissing: false,
+                    multipleVersionsAvailable: false,
+                    persistsState: false,
+                    restricted: false,
+                    validationStatus: 'VALID'
+                },
+                status: {
+                    runStatus: 'ENABLED',
+                    validationStatus: 'VALID'
+                }
+            } as any);
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-no-bulletins',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const service = newState.controllerServices.find((s) => s.id === 'controller-service-no-bulletins') as any;
+            expect(service).toBeDefined();
+            expect(service.bulletins).toEqual([]);
+        });
+
+        it('should handle controller service with empty bulletins array', () => {
+            testState.controllerServices.push({
+                id: 'controller-service-empty-bulletins',
+                uri: 'test-uri-empty-bulletins',
+                bulletins: [],
+                revision: { version: 1, clientId: 'test' },
+                permissions: { canRead: true, canWrite: true },
+                component: {
+                    id: 'controller-service-empty-bulletins',
+                    name: 'Test Controller Service Empty Bulletins',
+                    type: 'org.apache.nifi.TestControllerService',
+                    bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                    state: 'ENABLED',
+                    properties: {},
+                    descriptors: {},
+                    validationErrors: [],
+                    referencingComponents: [],
+                    comments: '',
+                    deprecated: false,
+                    extensionMissing: false,
+                    multipleVersionsAvailable: false,
+                    persistsState: false,
+                    restricted: false,
+                    validationStatus: 'VALID'
+                },
+                status: {
+                    runStatus: 'ENABLED',
+                    validationStatus: 'VALID'
+                }
+            } as any);
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-empty-bulletins',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const service = newState.controllerServices.find(
+                (s) => s.id === 'controller-service-empty-bulletins'
+            ) as any;
+            expect(service).toBeDefined();
+            expect(service.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent controller service gracefully', () => {
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'non-existent-service',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+
+        it('should handle empty controller services array gracefully', () => {
+            const emptyState: ControllerServicesState = {
+                ...initialState,
+                controllerServices: []
+            };
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'any-service',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(emptyState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(emptyState);
+        });
+
+        it('should preserve all other controller service properties when filtering bulletins', () => {
+            const originalService = testState.controllerServices[0];
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = controllerServicesReducer(testState, action);
+
+            const updatedService = newState.controllerServices.find((s) => s.id === 'controller-service-1');
+            expect(updatedService).toBeDefined();
+
+            // All properties except bulletins should remain the same
+            expect(updatedService!.id).toBe(originalService.id);
+            expect(updatedService!.uri).toBe(originalService.uri);
+            expect(updatedService!.revision).toEqual(originalService.revision);
+            expect(updatedService!.permissions).toEqual(originalService.permissions);
+            expect(updatedService!.component).toEqual(originalService.component);
+            expect(updatedService!.status).toEqual(originalService.status);
+
+            // Only bulletins should be filtered
+            expect((updatedService as any).bulletins).toHaveLength(1);
+            expect((updatedService as any).bulletins[0].id).toBe(2);
+        });
+
+        it('should handle multiple bulletin clearing operations correctly', () => {
+            // First operation - clear service 1
+            const action1 = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const intermediateState = controllerServicesReducer(testState, action1);
+
+            // Second operation - clear service 2
+            const action2 = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-2',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(4, testTimestamp), createTestBulletin(5, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const finalState = controllerServicesReducer(intermediateState, action2);
+
+            // Both services should be updated correctly
+            const service1 = finalState.controllerServices.find((s) => s.id === 'controller-service-1') as any;
+            expect(service1.bulletins).toHaveLength(1);
+            expect(service1.bulletins[0].id).toBe(2);
+
+            const service2 = finalState.controllerServices.find((s) => s.id === 'controller-service-2') as any;
+            expect(service2.bulletins).toHaveLength(2);
+            expect(service2.bulletins.map((b: any) => b.id)).toEqual([4, 5]);
+
+            // Service 3 should remain unchanged
+            const service3 = finalState.controllerServices.find((s) => s.id === 'controller-service-3') as any;
+            expect(service3.bulletins).toHaveLength(1);
+            expect(service3.bulletins[0].id).toBe(6);
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.ts
index a88f8ec9fe..f04861fbc7 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.reducer.ts
@@ -17,6 +17,7 @@
 
 import { createReducer, on } from '@ngrx/store';
 import {
+    clearControllerServiceBulletinsSuccess,
     configureControllerService,
     configureControllerServiceSuccess,
     controllerServicesBannerApiError,
@@ -114,5 +115,17 @@ export const controllerServicesReducer = createReducer(
             }
             draftState.saving = false;
         });
+    }),
+    on(clearControllerServiceBulletinsSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.controllerServices.findIndex(
+                (service: any) => service.id === response.componentId
+            );
+            if (componentIndex > -1) {
+                const service = draftState.controllerServices[componentIndex];
+                // Replace bulletins with the current bulletins from the server
+                service.bulletins = response.bulletins || [];
+            }
+        });
     })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 232f4ab422..9ea7f5769e 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -20,6 +20,8 @@ import {
     CenterComponentRequest,
     ChangeColorRequest,
     ChangeVersionDialogRequest,
+    ClearBulletinsForGroupRequest,
+    ClearBulletinsForGroupResponse,
     ComponentEntity,
     ConfirmStopVersionControlRequest,
     CreateComponentRequest,
@@ -110,9 +112,15 @@ import {
     VersionControlInformationEntity
 } from './index';
 import { StatusHistoryRequest } from '../../../../state/status-history';
-import { FetchComponentVersionsRequest, RegistryClientEntity } from '../../../../state/shared';
+import {
+    ClearBulletinsRequest,
+    ClearBulletinsResponse,
+    FetchComponentVersionsRequest,
+    RegistryClientEntity
+} from '../../../../state/shared';
 import { ErrorContext } from '../../../../state/error';
 import { CopyResponseContext, CopyResponseEntity } from '../../../../state/copy';
+import { BulletinEntity } from '@nifi/shared';
 
 const CANVAS_PREFIX = '[Canvas]';
 
@@ -657,6 +665,30 @@ export const runOnce = createAction(`${CANVAS_PREFIX} Run Once`, props<{ request
 
 export const runOnceSuccess = createAction(`${CANVAS_PREFIX} Run Once Success`, props<{ response: RunOnceResponse }>());
 
+/*
+    Clear Bulletins
+*/
+
+export const clearBulletinsForComponent = createAction(
+    `${CANVAS_PREFIX} Clear Bulletins For Component`,
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearBulletinsForComponentSuccess = createAction(
+    `${CANVAS_PREFIX} Clear Bulletins For Component Success`,
+    props<{ response: ClearBulletinsResponse }>()
+);
+
+export const clearBulletinsForProcessGroup = createAction(
+    `${CANVAS_PREFIX} Clear Bulletins For Process Group`,
+    props<{ request: ClearBulletinsForGroupRequest }>()
+);
+
+export const clearBulletinsForProcessGroupSuccess = createAction(
+    `${CANVAS_PREFIX} Clear Bulletins For Process Group Success`,
+    props<{ response: ClearBulletinsForGroupResponse }>()
+);
+
 export const startComponent = createAction(
     `${CANVAS_PREFIX} Start Component`,
     props<{ request: StartComponentRequest | StartProcessGroupRequest }>()
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
index 2bddcbed21..8c4a2d9a1d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
@@ -14,11 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import { FlowService } from '../../service/flow.service';
 import * as FlowActions from './flow.actions';
 import { of, ReplaySubject, take, throwError } from 'rxjs';
 import { MatDialog, MatDialogRef } from '@angular/material/dialog';
-import { ComponentHistoryEntity } from '../../../../state/shared';
+import { ClearBulletinsResponse, ComponentHistoryEntity } from '../../../../state/shared';
 import { EditProcessor } from '../../ui/canvas/items/processor/edit-processor/edit-processor.component';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
 import { FlowEffects } from './flow.effects';
@@ -801,7 +802,8 @@ describe('FlowEffects', () => {
                         getProcessor: jest.fn(),
                         updateComponent: jest.fn(),
                         createConnection: jest.fn(),
-                        createLabel: jest.fn()
+                        createLabel: jest.fn(),
+                        clearBulletinsForProcessGroup: jest.fn()
                     }
                 },
                 {
@@ -1092,4 +1094,92 @@ describe('FlowEffects', () => {
             });
         });
     });
+
+    describe('clearBulletinsForProcessGroup$', () => {
+        beforeEach(() => {
+            effects = TestBed.inject(FlowEffects);
+        });
+
+        it('should call flowService.clearBulletinsForProcessGroup and dispatch success action', async () => {
+            const request = {
+                processGroupId: 'test-group-id',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                components: ['component-1', 'component-2']
+            };
+
+            const mockResponse = {
+                bulletinsCleared: 10
+            };
+
+            jest.spyOn(flowService, 'clearBulletinsForProcessGroup').mockReturnValue(of(mockResponse));
+
+            const action = FlowActions.clearBulletinsForProcessGroup({ request });
+            action$.next(action);
+
+            const result = await new Promise((resolve) =>
+                effects.clearBulletinsForProcessGroup$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(
+                FlowActions.clearBulletinsForProcessGroupSuccess({
+                    response: {
+                        processGroupId: 'test-group-id',
+                        bulletinsCleared: 10
+                    }
+                })
+            );
+            expect(flowService.clearBulletinsForProcessGroup).toHaveBeenCalledWith(request);
+        });
+    });
+
+    describe('clearBulletinsForProcessGroupSuccess$', () => {
+        beforeEach(() => {
+            effects = TestBed.inject(FlowEffects);
+        });
+
+        it('should dispatch reloadFlow when clearing bulletins for the currently viewed process group', async () => {
+            const currentGroupId = 'current-group-id';
+            store.overrideSelector(selectCurrentProcessGroupId, currentGroupId);
+            store.refreshState();
+
+            const response = {
+                processGroupId: currentGroupId,
+                bulletinsCleared: 5
+            };
+
+            const action = FlowActions.clearBulletinsForProcessGroupSuccess({ response });
+            action$.next(action);
+
+            const result = await new Promise((resolve) =>
+                effects.clearBulletinsForProcessGroupSuccess$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(FlowActions.reloadFlow());
+        });
+
+        it('should dispatch loadChildProcessGroup when clearing bulletins for a child process group', async () => {
+            const currentGroupId = 'current-group-id';
+            const childGroupId = 'child-group-id';
+            store.overrideSelector(selectCurrentProcessGroupId, currentGroupId);
+            store.refreshState();
+
+            const response = {
+                processGroupId: childGroupId,
+                bulletinsCleared: 3
+            };
+
+            const action = FlowActions.clearBulletinsForProcessGroupSuccess({ response });
+            action$.next(action);
+
+            const result = await new Promise((resolve) =>
+                effects.clearBulletinsForProcessGroupSuccess$.pipe(take(1)).subscribe(resolve)
+            );
+
+            expect(result).toEqual(
+                FlowActions.loadChildProcessGroup({
+                    request: { id: childGroupId }
+                })
+            );
+        });
+    });
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 912e0c4019..c1e169c8c4 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -4618,4 +4618,71 @@ export class FlowEffects {
             ),
         { dispatch: false }
     );
+
+    /*
+        Clear Bulletins Effects
+    */
+
+    clearBulletinsForComponent$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.clearBulletinsForComponent),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.flowService.clearBulletinForComponent(request)).pipe(
+                    map((response) =>
+                        FlowActions.clearBulletinsForComponentSuccess({
+                            response: {
+                                componentId: response.componentId,
+                                bulletinsCleared: response.bulletinsCleared,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) => of(this.snackBarOrFullScreenError(errorResponse)))
+                )
+            )
+        )
+    );
+
+    clearBulletinsForProcessGroup$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.clearBulletinsForProcessGroup),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.flowService.clearBulletinsForProcessGroup(request)).pipe(
+                    map((response) =>
+                        FlowActions.clearBulletinsForProcessGroupSuccess({
+                            response: {
+                                processGroupId: request.processGroupId,
+                                bulletinsCleared: response.bulletinsCleared
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) => of(this.snackBarOrFullScreenError(errorResponse)))
+                )
+            )
+        )
+    );
+
+    clearBulletinsForProcessGroupSuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.clearBulletinsForProcessGroupSuccess),
+            map((action) => action.response),
+            concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
+            switchMap(([response, currentProcessGroupId]) => {
+                // If we cleared bulletins for the currently viewed process group, reload the entire flow
+                if (response.processGroupId === currentProcessGroupId) {
+                    return of(FlowActions.reloadFlow());
+                } else {
+                    // If it's a child process group visible on the canvas, reload just that child
+                    return of(
+                        FlowActions.loadChildProcessGroup({
+                            request: { id: response.processGroupId }
+                        })
+                    );
+                }
+            })
+        )
+    );
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.spec.ts
new file mode 100644
index 0000000000..5a0454fe52
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.spec.ts
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { flowReducer, initialState } from './flow.reducer';
+import { clearBulletinsForComponentSuccess } from './flow.actions';
+import { FlowState } from './index';
+import { BulletinEntity, ComponentType } from '@nifi/shared';
+
+describe('Flow Reducer', () => {
+    describe('clearBulletinsForComponentSuccess', () => {
+        let testState: FlowState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'source1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'source1',
+                    sourceName: 'Test Source',
+                    sourceType: 'COMPONENT',
+                    level: 'ERROR',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialState,
+                flow: {
+                    ...initialState.flow,
+                    processGroupFlow: {
+                        ...initialState.flow.processGroupFlow,
+                        flow: {
+                            ...initialState.flow.processGroupFlow.flow,
+                            processors: [
+                                {
+                                    id: 'processor-1',
+                                    bulletins: [
+                                        createTestBulletin(1, olderTimestamp), // Should be removed
+                                        createTestBulletin(2, newerTimestamp) // Should be kept
+                                    ],
+                                    revision: { version: 1, clientId: 'test' },
+                                    permissions: { canRead: true, canWrite: true },
+                                    position: { x: 0, y: 0 },
+                                    component: { id: 'processor-1', type: 'Processor' }
+                                } as any
+                            ],
+                            inputPorts: [
+                                {
+                                    id: 'input-port-1',
+                                    bulletins: [
+                                        createTestBulletin(3, olderTimestamp), // Should be removed
+                                        createTestBulletin(4, testTimestamp), // Should be kept (equal to timestamp)
+                                        createTestBulletin(5, newerTimestamp) // Should be kept
+                                    ],
+                                    revision: { version: 1, clientId: 'test' },
+                                    permissions: { canRead: true, canWrite: true },
+                                    position: { x: 0, y: 0 },
+                                    component: { id: 'input-port-1', type: 'InputPort' }
+                                } as any
+                            ],
+                            outputPorts: [
+                                {
+                                    id: 'output-port-1',
+                                    bulletins: [
+                                        createTestBulletin(6, olderTimestamp) // Should be removed
+                                    ],
+                                    revision: { version: 1, clientId: 'test' },
+                                    permissions: { canRead: true, canWrite: true },
+                                    position: { x: 0, y: 0 },
+                                    component: { id: 'output-port-1', type: 'OutputPort' }
+                                } as any
+                            ],
+                            remoteProcessGroups: [
+                                {
+                                    id: 'remote-pg-1',
+                                    bulletins: [
+                                        createTestBulletin(7, newerTimestamp) // Should be kept
+                                    ],
+                                    revision: { version: 1, clientId: 'test' },
+                                    permissions: { canRead: true, canWrite: true },
+                                    position: { x: 0, y: 0 },
+                                    component: { id: 'remote-pg-1', type: 'RemoteProcessGroup' }
+                                } as any
+                            ]
+                        }
+                    }
+                }
+            };
+        });
+
+        it('should replace bulletins for processors', () => {
+            // Provide the bulletins that should remain after clearing
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'processor-1',
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    bulletinsCleared: 1,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const processor = newState.flow.processGroupFlow.flow.processors![0] as any;
+            expect(processor.bulletins).toHaveLength(1);
+            expect(processor.bulletins![0].id).toBe(2); // Only newer bulletin remains
+        });
+
+        it('should replace bulletins for input ports', () => {
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'input-port-1',
+                    bulletins: [createTestBulletin(4, testTimestamp), createTestBulletin(5, newerTimestamp)],
+                    bulletinsCleared: 1,
+                    componentType: ComponentType.InputPort
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const inputPort = newState.flow.processGroupFlow.flow.inputPorts![0] as any;
+            expect(inputPort.bulletins).toHaveLength(2);
+            expect(inputPort.bulletins!.map((b: any) => b.id)).toEqual([4, 5]); // Equal and newer bulletins remain
+        });
+
+        it('should filter older bulletins from output ports', () => {
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'output-port-1',
+                    bulletins: [],
+                    bulletinsCleared: 1,
+                    componentType: ComponentType.OutputPort
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const outputPort = newState.flow.processGroupFlow.flow.outputPorts![0] as any;
+            expect(outputPort.bulletins).toHaveLength(0); // All bulletins removed
+        });
+
+        it('should replace bulletins for remote process groups', () => {
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'remote-pg-1',
+                    bulletins: [createTestBulletin(7, newerTimestamp)],
+                    bulletinsCleared: 0,
+                    componentType: ComponentType.RemoteProcessGroup
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const remotePg = newState.flow.processGroupFlow.flow.remoteProcessGroups![0] as any;
+            expect(remotePg.bulletins).toHaveLength(1);
+            expect(remotePg.bulletins![0].id).toBe(7); // Newer bulletin remains
+        });
+
+        it('should not affect other components when updating specific component', () => {
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'processor-1',
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    bulletinsCleared: 1,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            // Other components should remain unchanged
+            const inputPort = newState.flow.processGroupFlow.flow.inputPorts![0] as any;
+            expect(inputPort.bulletins).toHaveLength(3); // Original count unchanged
+
+            const outputPort = newState.flow.processGroupFlow.flow.outputPorts![0] as any;
+            expect(outputPort.bulletins).toHaveLength(1); // Original count unchanged
+
+            const remotePg = newState.flow.processGroupFlow.flow.remoteProcessGroups![0] as any;
+            expect(remotePg.bulletins).toHaveLength(1); // Original count unchanged
+        });
+
+        it('should handle component with no bulletins', () => {
+            // Add a component with no bulletins
+            testState.flow.processGroupFlow.flow.processors!.push({
+                id: 'processor-no-bulletins',
+                bulletins: undefined,
+                revision: { version: 1, clientId: 'test' },
+                permissions: { canRead: true, canWrite: true },
+                position: { x: 0, y: 0 },
+                component: { id: 'processor-no-bulletins', type: 'Processor' }
+            } as any);
+
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'processor-no-bulletins',
+                    bulletins: [],
+                    bulletinsCleared: 0,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const processor = newState.flow.processGroupFlow.flow.processors!.find(
+                (p) => p.id === 'processor-no-bulletins'
+            ) as any;
+            expect(processor).toBeDefined();
+            expect(processor!.bulletins).toEqual([]);
+        });
+
+        it('should handle component with empty bulletins array', () => {
+            // Add a component with empty bulletins array
+            testState.flow.processGroupFlow.flow.processors!.push({
+                id: 'processor-empty-bulletins',
+                bulletins: [],
+                revision: { version: 1, clientId: 'test' },
+                permissions: { canRead: true, canWrite: true },
+                position: { x: 0, y: 0 },
+                component: { id: 'processor-empty-bulletins', type: 'Processor' }
+            } as any);
+
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'processor-empty-bulletins',
+                    bulletins: [],
+                    bulletinsCleared: 0,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            const processor = newState.flow.processGroupFlow.flow.processors!.find(
+                (p) => p.id === 'processor-empty-bulletins'
+            ) as any;
+            expect(processor).toBeDefined();
+            expect(processor!.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent component gracefully', () => {
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'non-existent-component',
+                    bulletins: [],
+                    bulletinsCleared: 0,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+
+        it('should handle missing collections gracefully', () => {
+            // Create state with missing collections
+            const stateWithMissingCollections: FlowState = {
+                ...initialState,
+                flow: {
+                    ...initialState.flow,
+                    processGroupFlow: {
+                        ...initialState.flow.processGroupFlow,
+                        flow: {
+                            ...initialState.flow.processGroupFlow.flow,
+                            processors: null as any,
+                            inputPorts: null as any,
+                            outputPorts: null as any,
+                            remoteProcessGroups: null as any
+                        }
+                    }
+                }
+            };
+
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'any-component',
+                    bulletins: [],
+                    bulletinsCleared: 0,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(stateWithMissingCollections, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(stateWithMissingCollections);
+        });
+
+        it('should break after finding and updating the first matching component', () => {
+            // Add duplicate component IDs across different collections to test break logic
+            testState.flow.processGroupFlow.flow.inputPorts!.push({
+                id: 'processor-1', // Same ID as processor
+                bulletins: [createTestBulletin(99, olderTimestamp)],
+                revision: { version: 1, clientId: 'test' },
+                permissions: { canRead: true, canWrite: true },
+                position: { x: 0, y: 0 },
+                component: { id: 'processor-1', type: 'InputPort' }
+            } as any);
+
+            const action = clearBulletinsForComponentSuccess({
+                response: {
+                    componentId: 'processor-1',
+                    bulletins: [createTestBulletin(2, newerTimestamp)],
+                    bulletinsCleared: 1,
+                    componentType: ComponentType.Processor
+                }
+            });
+
+            const newState = flowReducer(testState, action);
+
+            // Only processor should be updated (first match), input port should remain unchanged
+            const processor = newState.flow.processGroupFlow.flow.processors![0] as any;
+            expect(processor.bulletins).toHaveLength(1);
+            expect(processor.bulletins![0].id).toBe(2);
+
+            const inputPortWithSameId = newState.flow.processGroupFlow.flow.inputPorts!.find(
+                (p) => p.id === 'processor-1'
+            ) as any;
+            expect(inputPortWithSameId!.bulletins).toHaveLength(1);
+            expect(inputPortWithSameId!.bulletins![0].id).toBe(99); // Unchanged
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
index 2cd7b24ff8..4b1a01c160 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
@@ -19,6 +19,7 @@ import { createReducer, on } from '@ngrx/store';
 import {
     changeVersionComplete,
     changeVersionSuccess,
+    clearBulletinsForComponentSuccess,
     createComponentComplete,
     createComponentSuccess,
     createConnection,
@@ -90,7 +91,7 @@ import {
     uploadProcessGroup
 } from './flow.actions';
 import { ComponentEntity, FlowState } from './index';
-import { ComponentType } from '@nifi/shared';
+import { ComponentType, BulletinEntity } from '@nifi/shared';
 import { produce } from 'immer';
 
 export const initialState: FlowState = {
@@ -652,7 +653,32 @@ export const flowReducer = createReducer(
     on(changeVersionComplete, revertChangesComplete, (state) => ({
         ...state,
         changeVersionRequest: null
-    }))
+    })),
+    on(clearBulletinsForComponentSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            // Find and update the component in all possible collections
+            const collections = [
+                { name: 'processors', array: draftState.flow.processGroupFlow.flow.processors },
+                { name: 'inputPorts', array: draftState.flow.processGroupFlow.flow.inputPorts },
+                { name: 'outputPorts', array: draftState.flow.processGroupFlow.flow.outputPorts },
+                { name: 'remoteProcessGroups', array: draftState.flow.processGroupFlow.flow.remoteProcessGroups }
+            ];
+
+            for (const collection of collections) {
+                if (collection.array) {
+                    const componentIndex = collection.array.findIndex(
+                        (component: any) => component.id === response.componentId
+                    );
+                    if (componentIndex > -1) {
+                        const component = collection.array[componentIndex] as any;
+                        // Replace bulletins with the current bulletins from the server
+                        component.bulletins = response.bulletins;
+                        break; // Component found and updated, no need to continue
+                    }
+                }
+            }
+        });
+    })
 );
 
 function getComponentCollection(draftState: FlowState, componentType: ComponentType): any[] | null {
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 5fb3956586..d0ae7391bc 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -836,6 +836,20 @@ export interface LoadChildProcessGroupRequest {
     id: string;
 }
 
+/*
+  Clear Bulletins
+*/
+
+export interface ClearBulletinsForGroupRequest {
+    processGroupId: string;
+    fromTimestamp: string;
+}
+
+export interface ClearBulletinsForGroupResponse {
+    processGroupId: string;
+    bulletinsCleared: number;
+}
+
 export interface FlowUpdateRequest {
     requestId: string;
     processGroupId: string;
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
index fad36f3488..eb8763390d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
@@ -85,7 +85,10 @@
                                 (viewStateControllerService)="viewStateControllerService($event)"
                                 (changeControllerServiceVersion)="changeControllerServiceVersion($event)"
                                 (goToControllerService)="navigateToControllerService($event)"
-                                (deleteControllerService)="deleteControllerService($event)"></controller-service-table>
+                                (deleteControllerService)="deleteControllerService($event)"
+                                (clearBulletinsControllerService)="
+                                    clearBulletinsControllerService($event)
+                                "></controller-service-table>
                         </div>
                     }
                     <div class="flex justify-between">
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
index 849390d6e7..1613caf03d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
@@ -17,9 +17,9 @@
 
 import { TestBed } from '@angular/core/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
-
 import { ControllerServices } from './controller-services.component';
-import { provideMockStore, MockStore } from '@ngrx/store/testing';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../state/controller-services/controller-services.reducer';
 import { RouterTestingModule } from '@angular/router/testing';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { MockComponent } from 'ng-mocks';
@@ -28,6 +28,11 @@ import { canvasFeatureKey } from '../../state';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
 import { ControllerServiceEntity } from '../../../../state/shared';
+import { ComponentType } from '@nifi/shared';
+import {
+    clearControllerServiceBulletins,
+    loadControllerServices
+} from '../../state/controller-services/controller-services.actions';
 import { BreadcrumbEntity } from '../../state/shared';
 import { FormsModule } from '@angular/forms';
 import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -37,6 +42,69 @@ import { ControllerServiceTable } from '../../../../ui/common/controller-service
 
 describe('ControllerServices', () => {
     // Mock data factories
+    function createMockControllerServiceEntity(): ControllerServiceEntity {
+        return {
+            id: 'test-controller-service-id',
+            uri: 'test-uri',
+            bulletins: [
+                {
+                    id: 1,
+                    sourceId: 'test-controller-service-id',
+                    groupId: 'test-group-id',
+                    timestamp: '12:00:00 UTC',
+                    timestampIso: '2023-10-08T12:00:00.000Z',
+                    canRead: true,
+                    bulletin: {
+                        id: 1,
+                        category: 'INFO',
+                        level: 'INFO',
+                        message: 'Test bulletin',
+                        timestamp: '12:00:00 UTC',
+                        sourceId: 'test-controller-service-id',
+                        groupId: 'test-group-id',
+                        sourceType: 'CONTROLLER_SERVICE',
+                        sourceName: 'Test Controller Service'
+                    }
+                }
+            ],
+            permissions: {
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 1,
+                clientId: 'test-client'
+            },
+            component: {
+                id: 'test-controller-service-id',
+                name: 'Test Controller Service',
+                type: 'org.apache.nifi.TestControllerService',
+                bundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                state: 'ENABLED',
+                properties: {},
+                descriptors: {},
+                validationErrors: [],
+                referencingComponents: [],
+                comments: '',
+                deprecated: false,
+                extensionMissing: false,
+                multipleVersionsAvailable: false,
+                persistsState: false,
+                restricted: false,
+                validationStatus: 'VALID'
+            },
+            status: {
+                runStatus: 'ENABLED',
+                validationStatus: 'VALID'
+            },
+            parentGroupId: 'test-process-group-id'
+        } as ControllerServiceEntity;
+    }
+
     function createMockBreadcrumb(
         options: {
             id?: string;
@@ -169,7 +237,7 @@ describe('ControllerServices', () => {
     async function setup(options: SetupOptions = {}) {
         const { controllerServicesState = createMockState(), mockStore = {} } = options;
 
-        const initialState = {
+        const testInitialState = {
             [canvasFeatureKey]: {
                 [controllerServicesFeatureKey]: controllerServicesState
             },
@@ -209,14 +277,13 @@ describe('ControllerServices', () => {
                 MockComponent(Breadcrumbs),
                 MockComponent(ControllerServiceTable)
             ],
-            providers: [provideMockStore({ initialState })],
+            providers: [provideMockStore({ initialState: testInitialState })],
             schemas: [NO_ERRORS_SCHEMA]
         }).compileComponents();
 
         const fixture = TestBed.createComponent(ControllerServices);
         const component = fixture.componentInstance;
         const store = TestBed.inject(MockStore);
-
         fixture.detectChanges();
 
         return { component, fixture, store };
@@ -231,6 +298,205 @@ describe('ControllerServices', () => {
             const { component } = await setup();
             expect(component).toBeTruthy();
         });
+
+        it('should return true for isInitialLoading when timestamp matches initial state', async () => {
+            const { component } = await setup();
+            const state = { ...initialState };
+
+            expect(component.isInitialLoading(state)).toBe(true);
+        });
+
+        it('should return false for isInitialLoading when timestamp differs from initial state', async () => {
+            const { component } = await setup();
+            const state = {
+                ...initialState,
+                loadedTimestamp: '2023-01-01 12:00:00 EST'
+            };
+
+            expect(component.isInitialLoading(state)).toBe(false);
+        });
+    });
+
+    describe('Action dispatching', () => {
+        it('should dispatch loadControllerServices action when refreshControllerServiceListing is called', async () => {
+            const { component, store } = await setup();
+            jest.spyOn(store, 'dispatch');
+
+            // Set the current process group ID
+            (component as any).currentProcessGroupId = 'test-process-group-id';
+
+            component.refreshControllerServiceListing();
+
+            expect(store.dispatch).toHaveBeenCalledWith(
+                loadControllerServices({
+                    request: {
+                        processGroupId: 'test-process-group-id'
+                    }
+                })
+            );
+        });
+
+        it('should dispatch clearControllerServiceBulletins action when clearBulletinsControllerService is called', async () => {
+            const { component, store } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            jest.spyOn(store, 'dispatch');
+
+            component.clearBulletinsControllerService(mockControllerServiceEntity);
+
+            expect(store.dispatch).toHaveBeenCalledWith(
+                clearControllerServiceBulletins({
+                    request: {
+                        uri: mockControllerServiceEntity.uri,
+                        fromTimestamp: '2023-10-08T12:00:00.000Z',
+                        componentId: mockControllerServiceEntity.id,
+                        componentType: ComponentType.ControllerService
+                    }
+                })
+            );
+        });
+    });
+
+    describe('Breadcrumb and scope functions', () => {
+        it('should format scope correctly for controller service entity', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const formatScopeFn = component.formatScope(mockBreadcrumb);
+            const result = formatScopeFn(mockControllerServiceEntity);
+
+            expect(result).toBe('Test Process Group');
+        });
+
+        it('should return entity ID when breadcrumb cannot be read', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: false, canWrite: false },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const formatScopeFn = component.formatScope(mockBreadcrumb);
+            const result = formatScopeFn(mockControllerServiceEntity);
+
+            expect(result).toBe('test-process-group-id');
+        });
+
+        it('should return empty string when breadcrumb is not found', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            mockControllerServiceEntity.parentGroupId = 'different-group-id';
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const formatScopeFn = component.formatScope(mockBreadcrumb);
+            const result = formatScopeFn(mockControllerServiceEntity);
+
+            expect(result).toBe('');
+        });
+
+        it('should return true when entity is defined by current group', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const definedByCurrentGroupFn = component.definedByCurrentGroup(mockBreadcrumb);
+            const result = definedByCurrentGroupFn(mockControllerServiceEntity);
+
+            expect(result).toBe(true);
+        });
+
+        it('should return false when entity is not defined by current group', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            mockControllerServiceEntity.parentGroupId = 'different-group-id';
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const definedByCurrentGroupFn = component.definedByCurrentGroup(mockBreadcrumb);
+            const result = definedByCurrentGroupFn(mockControllerServiceEntity);
+
+            expect(result).toBe(false);
+        });
+    });
+
+    describe('Permission functions', () => {
+        it('should return true when user can modify parent group', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const canModifyParentFn = component.canModifyParent(mockBreadcrumb);
+            const result = canModifyParentFn(mockControllerServiceEntity);
+
+            expect(result).toBe(true);
+        });
+
+        it('should return false when user cannot modify parent group', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: false },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const canModifyParentFn = component.canModifyParent(mockBreadcrumb);
+            const result = canModifyParentFn(mockControllerServiceEntity);
+
+            expect(result).toBe(false);
+        });
+
+        it('should return false when breadcrumb is not found for permission check', async () => {
+            const { component } = await setup();
+            const mockControllerServiceEntity = createMockControllerServiceEntity();
+            mockControllerServiceEntity.parentGroupId = 'different-group-id';
+            const mockBreadcrumb = {
+                id: 'test-process-group-id',
+                breadcrumb: { id: 'test-process-group-id', name: 'Test Process Group' },
+                permissions: { canRead: true, canWrite: true },
+                parentBreadcrumb: undefined,
+                versionedFlowState: 'UNVERSIONED'
+            };
+
+            const canModifyParentFn = component.canModifyParent(mockBreadcrumb);
+            const result = canModifyParentFn(mockControllerServiceEntity);
+
+            expect(result).toBe(false);
+        });
     });
 
     describe('Filtering functionality', () => {
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.ts
index ba9e6a2388..b501dceed4 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.ts
@@ -28,6 +28,7 @@ import {
 } from '../../state/controller-services/controller-services.selectors';
 import { ControllerServicesState } from '../../state/controller-services';
 import {
+    clearControllerServiceBulletins,
     loadControllerServices,
     navigateToAdvancedServiceUi,
     navigateToEditService,
@@ -43,7 +44,7 @@ import {
     selectControllerService
 } from '../../state/controller-services/controller-services.actions';
 import { initialState } from '../../state/controller-services/controller-services.reducer';
-import { ComponentType, isDefinedAndNotNull, TextTip } from '@nifi/shared';
+import { ComponentType, isDefinedAndNotNull, NiFiCommon, TextTip } from '@nifi/shared';
 import { ControllerServiceEntity } from '../../../../state/shared';
 import { BreadcrumbEntity } from '../../state/shared';
 import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
@@ -62,6 +63,7 @@ import { DocumentationRequest } from '../../../../state/documentation';
 })
 export class ControllerServices implements OnDestroy {
     private store = inject<Store<NiFiState>>(Store);
+    private nifiCommon = inject(NiFiCommon);
 
     // Convert observables to signals
     serviceState = this.store.selectSignal(selectControllerServicesState);
@@ -361,6 +363,26 @@ export class ControllerServices implements OnDestroy {
         );
     }
 
+    clearBulletinsControllerService(entity: ControllerServiceEntity): void {
+        // Get the most recent bulletin timestamp from the entity's bulletins
+        // This will be reconstructed from the time-only string to a full timestamp
+        const fromTimestamp = this.nifiCommon.getMostRecentBulletinTimestamp(entity.bulletins || []);
+        if (fromTimestamp === null) {
+            return; // no bulletins to clear
+        }
+
+        this.store.dispatch(
+            clearControllerServiceBulletins({
+                request: {
+                    uri: entity.uri,
+                    fromTimestamp,
+                    componentId: entity.id,
+                    componentType: ComponentType.ControllerService
+                }
+            })
+        );
+    }
+
     // Filter methods
     toggleFilter(): void {
         this.showCurrentScopeOnly.set(!this.showCurrentScopeOnly());
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/flow-analysis-rule.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/flow-analysis-rule.service.ts
index 9b03e51fb5..d8cbaaf358 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/flow-analysis-rule.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/flow-analysis-rule.service.ts
@@ -82,6 +82,13 @@ export class FlowAnalysisRuleService implements PropertyDescriptorRetriever {
         );
     }
 
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
+
     setEnable(flowAnalysisRule: EnableFlowAnalysisRuleRequest, enabled: boolean): Observable<any> {
         const entity: FlowAnalysisRuleEntity = flowAnalysisRule.flowAnalysisRule;
         return this.httpClient.put(`${this.nifiCommon.stripProtocol(entity.uri)}/run-status`, {
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/management-controller-service.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/management-controller-service.service.ts
index 3722999123..01b9efe37c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/management-controller-service.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/management-controller-service.service.ts
@@ -93,4 +93,11 @@ export class ManagementControllerServiceService implements ControllerServiceCrea
         });
         return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params });
     }
+
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/parameter-provider.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/parameter-provider.service.ts
index ac532a9cef..7eec4045f6 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/parameter-provider.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/parameter-provider.service.ts
@@ -71,6 +71,13 @@ export class ParameterProviderService implements PropertyDescriptorRetriever {
         return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params });
     }
 
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
+
     getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
         const params: any = {
             propertyName,
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/registry-client.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/registry-client.service.ts
index bc3631d3dd..50b3749f60 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/registry-client.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/registry-client.service.ts
@@ -69,4 +69,11 @@ export class RegistryClientService implements PropertyDescriptorRetriever {
         });
         return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params });
     }
+
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/reporting-task.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/reporting-task.service.ts
index 80aceb0c08..5b0894c62b 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/reporting-task.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/service/reporting-task.service.ts
@@ -108,4 +108,11 @@ export class ReportingTaskService implements PropertyDescriptorRetriever {
             configureReportingTask.payload
         );
     }
+
+    clearBulletins(request: { uri: string; fromTimestamp: string }): Observable<any> {
+        const payload = {
+            fromTimestamp: request.fromTimestamp
+        };
+        return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.uri)}/bulletins/clear-requests`, payload);
+    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.actions.ts
index 6046cdeb94..89d3489ba6 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.actions.ts
@@ -31,7 +31,7 @@ import {
     LoadFlowAnalysisRulesResponse,
     SelectFlowAnalysisRuleRequest
 } from './index';
-import { FetchComponentVersionsRequest } from '../../../../state/shared';
+import { ClearBulletinsRequest, ClearBulletinsResponse, FetchComponentVersionsRequest } from '../../../../state/shared';
 
 export const resetFlowAnalysisRulesState = createAction('[Flow Analysis Rules] Reset Flow Analysis Rules State');
 
@@ -137,3 +137,13 @@ export const openChangeFlowAnalysisRuleVersionDialog = createAction(
     `[Flow Analysis Rules] Open Change Flow Analysis Rule Version Dialog`,
     props<{ request: FetchComponentVersionsRequest }>()
 );
+
+export const clearFlowAnalysisRuleBulletins = createAction(
+    '[Flow Analysis Rules] Clear Flow Analysis Rule Bulletins',
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearFlowAnalysisRuleBulletinsSuccess = createAction(
+    '[Flow Analysis Rules] Clear Flow Analysis Rule Bulletins Success',
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.spec.ts
index f57c5d79c7..32227c0ee9 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,186 +17,616 @@
 
 import { TestBed } from '@angular/core/testing';
 import { provideMockActions } from '@ngrx/effects/testing';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockStore } from '@ngrx/store/testing';
 import { Action } from '@ngrx/store';
-import { ReplaySubject, of, take, throwError } from 'rxjs';
+import { ReplaySubject, of, throwError, firstValueFrom } from 'rxjs';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
 import { HttpErrorResponse } from '@angular/common/http';
 
-import * as FlowAnalysisRuleActions from './flow-analysis-rules.actions';
 import { FlowAnalysisRulesEffects } from './flow-analysis-rules.effects';
+import * as FlowAnalysisRulesActions from './flow-analysis-rules.actions';
 import { FlowAnalysisRuleService } from '../../service/flow-analysis-rule.service';
 import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
 import { ExtensionTypesService } from '../../../../service/extension-types.service';
+import { ComponentType } from '@nifi/shared';
+import { FlowAnalysisRuleEntity, flowAnalysisRulesFeatureKey } from './index';
 import { initialState } from './flow-analysis-rules.reducer';
 import { settingsFeatureKey } from '../index';
-import { flowAnalysisRulesFeatureKey } from './index';
+import * as ErrorActions from '../../../../state/error/error.actions';
 
 describe('FlowAnalysisRulesEffects', () => {
-    interface SetupOptions {
-        flowAnalysisRulesState?: any;
-    }
+    let actions$: ReplaySubject<Action>;
+    let effects: FlowAnalysisRulesEffects;
+    let mockFlowAnalysisRuleService: jest.Mocked<FlowAnalysisRuleService>;
+    let mockErrorHelper: jest.Mocked<ErrorHelper>;
+    let mockDialog: jest.Mocked<MatDialog>;
+
+    beforeEach(() => {
+        const flowAnalysisRuleServiceSpy = {
+            getFlowAnalysisRule: jest.fn(),
+            createFlowAnalysisRule: jest.fn(),
+            updateFlowAnalysisRule: jest.fn(),
+            deleteFlowAnalysisRule: jest.fn(),
+            setEnable: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<FlowAnalysisRuleService>;
+
+        const errorHelperSpy = {
+            handleLoadingError: jest.fn().mockReturnValue({ type: '[Error] Loading Error' }),
+            getErrorString: jest.fn().mockReturnValue('Test error message'),
+            showErrorInContext: jest.fn(),
+            fullScreenError: jest.fn().mockReturnValue({ type: 'FULL_SCREEN_ERROR' })
+        } as unknown as jest.Mocked<ErrorHelper>;
+
+        const dialogSpy = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        } as unknown as jest.Mocked<MatDialog>;
+
+        const routerSpy = {
+            navigate: jest.fn()
+        } as unknown as jest.Mocked<Router>;
+
+        const propertyTableHelperServiceSpy = {
+            getComponentHistory: jest.fn(),
+            createNewProperty: jest.fn(),
+            createNewService: jest.fn()
+        } as unknown as jest.Mocked<PropertyTableHelperService>;
 
-    const mockResponse = {
-        flowAnalysisRules: [],
-        currentTime: '2023-01-01 12:00:00 EST'
-    };
+        const extensionTypesServiceSpy = {
+            getFlowAnalysisRuleVersionsForType: jest.fn()
+        } as unknown as jest.Mocked<ExtensionTypesService>;
 
-    async function setup({ flowAnalysisRulesState = initialState }: SetupOptions = {}) {
-        await TestBed.configureTestingModule({
+        TestBed.configureTestingModule({
             providers: [
                 FlowAnalysisRulesEffects,
-                provideMockActions(() => action$),
+                provideMockActions(() => actions$),
                 provideMockStore({
                     initialState: {
                         [settingsFeatureKey]: {
-                            [flowAnalysisRulesFeatureKey]: flowAnalysisRulesState
+                            [flowAnalysisRulesFeatureKey]: initialState
                         }
                     }
                 }),
-                { provide: FlowAnalysisRuleService, useValue: { getFlowAnalysisRule: jest.fn() } },
+                { provide: FlowAnalysisRuleService, useValue: flowAnalysisRuleServiceSpy },
                 { provide: ManagementControllerServiceService, useValue: {} },
-                { provide: ErrorHelper, useValue: { handleLoadingError: jest.fn() } },
-                { provide: MatDialog, useValue: { open: jest.fn(), closeAll: jest.fn() } },
-                { provide: Router, useValue: { navigate: jest.fn() } },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} }
+                { provide: ErrorHelper, useValue: errorHelperSpy },
+                { provide: Router, useValue: routerSpy },
+                { provide: MatDialog, useValue: dialogSpy },
+                { provide: PropertyTableHelperService, useValue: propertyTableHelperServiceSpy },
+                { provide: ExtensionTypesService, useValue: extensionTypesServiceSpy }
             ]
-        }).compileComponents();
+        });
 
-        const store = TestBed.inject(MockStore);
-        const effects = TestBed.inject(FlowAnalysisRulesEffects);
-        const flowAnalysisRuleService = TestBed.inject(FlowAnalysisRuleService);
-        const errorHelper = TestBed.inject(ErrorHelper);
+        effects = TestBed.inject(FlowAnalysisRulesEffects);
+        mockFlowAnalysisRuleService = TestBed.inject(FlowAnalysisRuleService) as jest.Mocked<FlowAnalysisRuleService>;
+        mockErrorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        mockDialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        actions$ = new ReplaySubject(1);
+    });
 
-        return { effects, store, flowAnalysisRuleService, errorHelper };
-    }
+    describe('loadFlowAnalysisRule$', () => {
+        it('should dispatch loadFlowAnalysisRulesSuccess action when flow analysis rules are loaded successfully', async () => {
+            const mockResponse = {
+                flowAnalysisRules: [createMockFlowAnalysisRuleEntity()],
+                currentTime: '2023-01-01 12:00:00 EST'
+            };
+            mockFlowAnalysisRuleService.getFlowAnalysisRule.mockReturnValue(of(mockResponse));
 
-    let action$: ReplaySubject<Action>;
-    beforeEach(() => {
-        action$ = new ReplaySubject<Action>();
-    });
+            actions$.next(FlowAnalysisRulesActions.loadFlowAnalysisRules());
+
+            const result = await firstValueFrom(effects.loadFlowAnalysisRule$);
+
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.loadFlowAnalysisRulesSuccess({
+                    response: {
+                        flowAnalysisRules: mockResponse.flowAnalysisRules,
+                        loadedTimestamp: mockResponse.currentTime
+                    }
+                })
+            );
+            expect(mockFlowAnalysisRuleService.getFlowAnalysisRule).toHaveBeenCalled();
+        });
+
+        it('should dispatch loadFlowAnalysisRulesError with status pending on initial load failure', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockFlowAnalysisRuleService.getFlowAnalysisRule.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(FlowAnalysisRulesActions.loadFlowAnalysisRules());
 
-    it('should create', async () => {
-        const { effects } = await setup();
-        expect(effects).toBeTruthy();
+            const result = await firstValueFrom(effects.loadFlowAnalysisRule$);
+
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.loadFlowAnalysisRulesError({
+                    errorResponse,
+                    loadedTimestamp: initialState.loadedTimestamp,
+                    status: 'pending'
+                })
+            );
+        });
     });
 
-    describe('Load Flow Analysis Rules', () => {
-        it('should load flow analysis rules successfully', async () => {
-            const { effects, flowAnalysisRuleService } = await setup();
+    describe('loadFlowAnalysisRulesError$', () => {
+        it('should handle error on initial load (hasExistingData=false)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = FlowAnalysisRulesActions.flowAnalysisRuleBannerApiError({ error: 'Error loading' });
 
-            action$.next(FlowAnalysisRuleActions.loadFlowAnalysisRules());
-            jest.spyOn(flowAnalysisRuleService, 'getFlowAnalysisRule').mockReturnValueOnce(of(mockResponse) as never);
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
 
-            const result = await new Promise((resolve) =>
-                effects.loadFlowAnalysisRule$.pipe(take(1)).subscribe(resolve)
+            actions$.next(
+                FlowAnalysisRulesActions.loadFlowAnalysisRulesError({
+                    errorResponse,
+                    loadedTimestamp: initialState.loadedTimestamp,
+                    status: 'pending'
+                })
             );
 
+            const result = await firstValueFrom(effects.loadFlowAnalysisRulesError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(false, errorResponse);
+            expect(result).toEqual(errorAction);
+        });
+
+        it('should handle error on refresh (hasExistingData=true)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = FlowAnalysisRulesActions.flowAnalysisRuleBannerApiError({ error: 'Error loading' });
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
+                FlowAnalysisRulesActions.loadFlowAnalysisRulesError({
+                    errorResponse,
+                    loadedTimestamp: '2023-01-01 11:00:00 EST',
+                    status: 'success'
+                })
+            );
+
+            const result = await firstValueFrom(effects.loadFlowAnalysisRulesError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(true, errorResponse);
+            expect(result).toEqual(errorAction);
+        });
+    });
+
+    describe('createFlowAnalysisRule$', () => {
+        it('should dispatch createFlowAnalysisRuleSuccess action when flow analysis rule is created successfully', async () => {
+            const mockRequest = {
+                flowAnalysisRuleType: 'org.apache.nifi.TestFlowAnalysisRule',
+                flowAnalysisRuleBundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                revision: { version: 0, clientId: 'test-client' }
+            };
+            const mockResponse = createMockFlowAnalysisRuleEntity();
+            mockFlowAnalysisRuleService.createFlowAnalysisRule.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.createFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.createFlowAnalysisRule$);
+
             expect(result).toEqual(
-                FlowAnalysisRuleActions.loadFlowAnalysisRulesSuccess({
+                FlowAnalysisRulesActions.createFlowAnalysisRuleSuccess({
                     response: {
-                        flowAnalysisRules: mockResponse.flowAnalysisRules,
-                        loadedTimestamp: mockResponse.currentTime
+                        flowAnalysisRule: mockResponse
                     }
                 })
             );
+            expect(mockFlowAnalysisRuleService.createFlowAnalysisRule).toHaveBeenCalledWith(mockRequest);
         });
 
-        it('should fail to load flow analysis rules on initial load with hasExistingData=false', async () => {
-            const { effects, flowAnalysisRuleService } = await setup();
+        it('should handle error and close dialog when creating flow analysis rule fails', async () => {
+            const mockRequest = {
+                flowAnalysisRuleType: 'org.apache.nifi.TestFlowAnalysisRule',
+                flowAnalysisRuleBundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                revision: { version: 0, clientId: 'test-client' }
+            };
+            const errorResponse = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            mockFlowAnalysisRuleService.createFlowAnalysisRule.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(FlowAnalysisRulesActions.createFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.createFlowAnalysisRule$);
 
-            action$.next(FlowAnalysisRuleActions.loadFlowAnalysisRules());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(flowAnalysisRuleService, 'getFlowAnalysisRule').mockImplementationOnce(() =>
-                throwError(() => error)
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.flowAnalysisRuleSnackbarApiError({
+                    error: 'Test error message'
+                })
             );
+        });
+    });
+
+    describe('deleteFlowAnalysisRule$', () => {
+        it('should dispatch deleteFlowAnalysisRuleSuccess action when flow analysis rule is deleted successfully', async () => {
+            const mockRequest = {
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const mockResponse = createMockFlowAnalysisRuleEntity();
+            mockFlowAnalysisRuleService.deleteFlowAnalysisRule.mockReturnValue(of(mockResponse));
 
-            const result = await new Promise((resolve) =>
-                effects.loadFlowAnalysisRule$.pipe(take(1)).subscribe(resolve)
+            actions$.next(FlowAnalysisRulesActions.deleteFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.deleteFlowAnalysisRule$);
+
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.deleteFlowAnalysisRuleSuccess({
+                    response: {
+                        flowAnalysisRule: mockResponse
+                    }
+                })
             );
+            expect(mockFlowAnalysisRuleService.deleteFlowAnalysisRule).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error when deleting flow analysis rule fails', async () => {
+            const mockRequest = {
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockFlowAnalysisRuleService.deleteFlowAnalysisRule.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(FlowAnalysisRulesActions.deleteFlowAnalysisRule({ request: mockRequest }));
 
+            const result = await firstValueFrom(effects.deleteFlowAnalysisRule$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
             expect(result).toEqual(
-                FlowAnalysisRuleActions.loadFlowAnalysisRulesError({
-                    errorResponse: error,
-                    loadedTimestamp: initialState.loadedTimestamp,
-                    status: 'pending'
+                FlowAnalysisRulesActions.flowAnalysisRuleSnackbarApiError({
+                    error: 'Test error message'
                 })
             );
         });
+    });
 
-        it('should fail to load flow analysis rules on refresh with hasExistingData=true', async () => {
-            const stateWithData = { ...initialState, loadedTimestamp: '2023-01-01 11:00:00 EST' };
-            const { effects, flowAnalysisRuleService } = await setup({
-                flowAnalysisRulesState: stateWithData
-            });
+    describe('enableFlowAnalysisRule$', () => {
+        it('should dispatch enableFlowAnalysisRuleSuccess action when flow analysis rule is enabled successfully', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const mockResponse = createMockFlowAnalysisRuleEntity();
+            mockFlowAnalysisRuleService.setEnable.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.enableFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.enableFlowAnalysisRule$);
 
-            action$.next(FlowAnalysisRuleActions.loadFlowAnalysisRules());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(flowAnalysisRuleService, 'getFlowAnalysisRule').mockImplementationOnce(() =>
-                throwError(() => error)
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.enableFlowAnalysisRuleSuccess({
+                    response: {
+                        id: mockRequest.id,
+                        flowAnalysisRule: mockResponse
+                    }
+                })
             );
+            expect(mockFlowAnalysisRuleService.setEnable).toHaveBeenCalledWith(mockRequest, true);
+        });
 
-            const result = await new Promise((resolve) =>
-                effects.loadFlowAnalysisRule$.pipe(take(1)).subscribe(resolve)
+        it('should handle error when enabling flow analysis rule fails', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockFlowAnalysisRuleService.setEnable.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(FlowAnalysisRulesActions.enableFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.enableFlowAnalysisRule$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.flowAnalysisRuleSnackbarApiError({
+                    error: 'Test error message'
+                })
             );
+        });
+    });
+
+    describe('disableFlowAnalysisRule$', () => {
+        it('should dispatch disableFlowAnalysisRuleSuccess action when flow analysis rule is disabled successfully', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const mockResponse = createMockFlowAnalysisRuleEntity();
+            mockFlowAnalysisRuleService.setEnable.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.disableFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.disableFlowAnalysisRule$);
 
             expect(result).toEqual(
-                FlowAnalysisRuleActions.loadFlowAnalysisRulesError({
-                    errorResponse: error,
-                    loadedTimestamp: stateWithData.loadedTimestamp,
-                    status: 'success'
+                FlowAnalysisRulesActions.disableFlowAnalysisRuleSuccess({
+                    response: {
+                        id: mockRequest.id,
+                        flowAnalysisRule: mockResponse
+                    }
+                })
+            );
+            expect(mockFlowAnalysisRuleService.setEnable).toHaveBeenCalledWith(mockRequest, false);
+        });
+
+        it('should handle error when disabling flow analysis rule fails', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockFlowAnalysisRuleService.setEnable.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(FlowAnalysisRulesActions.disableFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.disableFlowAnalysisRule$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.flowAnalysisRuleSnackbarApiError({
+                    error: 'Test error message'
                 })
             );
         });
     });
 
-    describe('Load Flow Analysis Rules Error', () => {
-        it('should handle flow analysis rules error for initial load', async () => {
-            const { effects, errorHelper } = await setup();
+    describe('clearFlowAnalysisRuleBulletins$', () => {
+        it('should dispatch clearFlowAnalysisRuleBulletinsSuccess action when bulletins are cleared successfully', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowAnalysisRule
+            };
+            const mockResponse = {
+                bulletinsCleared: 5,
+                bulletins: []
+            };
+            mockFlowAnalysisRuleService.clearBulletins.mockReturnValue(of(mockResponse));
 
-            const error = new HttpErrorResponse({ status: 500 });
-            const errorAction = FlowAnalysisRuleActions.flowAnalysisRuleBannerApiError({ error: 'Error loading' });
-            action$.next(
-                FlowAnalysisRuleActions.loadFlowAnalysisRulesError({
-                    errorResponse: error,
-                    loadedTimestamp: initialState.loadedTimestamp,
-                    status: 'pending'
+            actions$.next(FlowAnalysisRulesActions.clearFlowAnalysisRuleBulletins({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.clearFlowAnalysisRuleBulletins$);
+
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.clearFlowAnalysisRuleBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: 5,
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+            expect(mockFlowAnalysisRuleService.clearBulletins).toHaveBeenCalledWith({
+                uri: mockRequest.uri,
+                fromTimestamp: mockRequest.fromTimestamp
+            });
+        });
+
+        it('should handle undefined bulletinsCleared response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowAnalysisRule
+            };
+            const mockResponse = {
+                bulletins: []
+            }; // No bulletinsCleared property
+            mockFlowAnalysisRuleService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.clearFlowAnalysisRuleBulletins({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.clearFlowAnalysisRuleBulletins$);
 
-            const result = await new Promise((resolve) =>
-                effects.loadFlowAnalysisRulesError$.pipe(take(1)).subscribe(resolve)
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.clearFlowAnalysisRuleBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: 0, // Should default to 0
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
+                })
             );
+        });
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(false, error);
-            expect(result).toEqual(errorAction);
+        it('should pass through all required fields in the success response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowAnalysisRule
+            };
+            const mockResponse = {
+                bulletinsCleared: 3,
+                bulletins: []
+            };
+            mockFlowAnalysisRuleService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.clearFlowAnalysisRuleBulletins({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.clearFlowAnalysisRuleBulletins$);
+
+            const successAction = result as any;
+            expect(successAction.response.componentId).toBe(mockRequest.componentId);
+            expect(successAction.response.componentType).toBe(mockRequest.componentType);
+            expect(successAction.response.bulletinsCleared).toBe(3);
+            expect(successAction.response.bulletins).toEqual([]);
+        });
+    });
+
+    describe('configureFlowAnalysisRule$', () => {
+        it('should dispatch configureFlowAnalysisRuleSuccess action when flow analysis rule is configured successfully', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client' },
+                    component: {
+                        id: 'test-id',
+                        name: 'Updated Flow Analysis Rule'
+                    }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const mockResponse = createMockFlowAnalysisRuleEntity();
+            mockFlowAnalysisRuleService.updateFlowAnalysisRule.mockReturnValue(of(mockResponse));
+
+            actions$.next(FlowAnalysisRulesActions.configureFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.configureFlowAnalysisRule$);
+
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.configureFlowAnalysisRuleSuccess({
+                    response: {
+                        id: mockRequest.id,
+                        flowAnalysisRule: mockResponse,
+                        postUpdateNavigation: mockRequest.postUpdateNavigation,
+                        postUpdateNavigationBoundary: mockRequest.postUpdateNavigationBoundary
+                    }
+                })
+            );
+            expect(mockFlowAnalysisRuleService.updateFlowAnalysisRule).toHaveBeenCalledWith(mockRequest);
         });
 
-        it('should handle flow analysis rules error for refresh', async () => {
-            const { effects, errorHelper } = await setup();
+        it('should handle error with banner when configuring flow analysis rule fails', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client' },
+                    component: {
+                        id: 'test-id',
+                        name: 'Updated Flow Analysis Rule'
+                    }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const errorResponse = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            mockFlowAnalysisRuleService.updateFlowAnalysisRule.mockReturnValue(throwError(() => errorResponse));
 
-            const error = new HttpErrorResponse({ status: 500 });
-            const errorAction = FlowAnalysisRuleActions.flowAnalysisRuleBannerApiError({ error: 'Error loading' });
-            action$.next(
-                FlowAnalysisRuleActions.loadFlowAnalysisRulesError({
-                    errorResponse: error,
-                    loadedTimestamp: '2023-01-01 11:00:00 EST',
-                    status: 'success'
+            actions$.next(FlowAnalysisRulesActions.configureFlowAnalysisRule({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.configureFlowAnalysisRule$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.flowAnalysisRuleBannerApiError({
+                    error: 'Test error message'
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+        });
+    });
+
+    describe('createFlowAnalysisRuleSuccess$', () => {
+        it('should close dialog and dispatch selectFlowAnalysisRule action', async () => {
+            const mockResponse = {
+                flowAnalysisRule: createMockFlowAnalysisRuleEntity()
+            };
+
+            actions$.next(FlowAnalysisRulesActions.createFlowAnalysisRuleSuccess({ response: mockResponse }));
+
+            const result = await firstValueFrom(effects.createFlowAnalysisRuleSuccess$);
 
-            const result = await new Promise((resolve) =>
-                effects.loadFlowAnalysisRulesError$.pipe(take(1)).subscribe(resolve)
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+            expect(result).toEqual(
+                FlowAnalysisRulesActions.selectFlowAnalysisRule({
+                    request: {
+                        id: mockResponse.flowAnalysisRule.id
+                    }
+                })
             );
+        });
+    });
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(true, error);
-            expect(result).toEqual(errorAction);
+    describe('flowAnalysisRuleBannerApiError$', () => {
+        it('should dispatch addBannerError action', async () => {
+            const errorMessage = 'Test error message';
+
+            actions$.next(FlowAnalysisRulesActions.flowAnalysisRuleBannerApiError({ error: errorMessage }));
+
+            const result = await firstValueFrom(effects.flowAnalysisRuleBannerApiError$);
+
+            expect(result).toEqual(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: [errorMessage],
+                        context: 'flow-analysis-rules' as any
+                    }
+                })
+            );
+        });
+    });
+
+    describe('flowAnalysisRuleSnackbarApiError$', () => {
+        it('should dispatch snackBarError action', async () => {
+            const errorMessage = 'Test error message';
+
+            actions$.next(FlowAnalysisRulesActions.flowAnalysisRuleSnackbarApiError({ error: errorMessage }));
+
+            const result = await firstValueFrom(effects.flowAnalysisRuleSnackbarApiError$);
+
+            expect(result).toEqual(ErrorActions.snackBarError({ error: errorMessage }));
         });
     });
+
+    // Mock data factory
+    function createMockFlowAnalysisRuleEntity(): FlowAnalysisRuleEntity {
+        return {
+            id: 'test-flow-analysis-rule-id',
+            uri: 'test-uri',
+            permissions: {
+                canRead: true,
+                canWrite: true
+            },
+            operatePermissions: {
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 1,
+                clientId: 'test-client'
+            },
+            component: {
+                id: 'test-flow-analysis-rule-id',
+                name: 'Test Flow Analysis Rule',
+                type: 'org.apache.nifi.TestFlowAnalysisRule',
+                bundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                state: 'DISABLED',
+                comments: 'Test comments',
+                persistsState: true,
+                restricted: false,
+                deprecated: false,
+                multipleVersionsAvailable: false,
+                supportsSensitiveDynamicProperties: false,
+                properties: {},
+                descriptors: {},
+                customUiUrl: '',
+                annotationData: '',
+                validationErrors: [],
+                validationStatus: 'VALID'
+            },
+            status: {
+                runStatus: 'DISABLED',
+                validationStatus: 'VALID'
+            },
+            bulletins: []
+        };
+    }
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts
index 04e317778a..fd7558daef 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts
@@ -575,4 +575,37 @@ export class FlowAnalysisRulesEffects {
             ),
         { dispatch: false }
     );
+
+    clearFlowAnalysisRuleBulletins$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowAnalysisRuleActions.clearFlowAnalysisRuleBulletins),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.flowAnalysisRuleService.clearBulletins({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    })
+                ).pipe(
+                    map((response) =>
+                        FlowAnalysisRuleActions.clearFlowAnalysisRuleBulletinsSuccess({
+                            response: {
+                                componentId: request.componentId,
+                                bulletinsCleared: response.bulletinsCleared || 0,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            FlowAnalysisRuleActions.flowAnalysisRuleSnackbarApiError({
+                                error: this.errorHelper.getErrorString(errorResponse)
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.spec.ts
new file mode 100644
index 0000000000..59862cdd79
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.spec.ts
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { flowAnalysisRulesReducer, initialState } from './flow-analysis-rules.reducer';
+import { clearFlowAnalysisRuleBulletinsSuccess } from './flow-analysis-rules.actions';
+import { FlowAnalysisRulesState } from './index';
+import { BulletinEntity, ComponentType } from '@nifi/shared';
+
+describe('Flow Analysis Rules Reducer', () => {
+    describe('clearFlowAnalysisRuleBulletinsSuccess', () => {
+        let testState: FlowAnalysisRulesState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'source1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'source1',
+                    sourceName: 'Test Source',
+                    sourceType: 'COMPONENT',
+                    level: 'TRACE',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialState,
+                flowAnalysisRules: [
+                    {
+                        id: 'flow-analysis-rule-1',
+                        uri: 'test-uri-1',
+                        status: { runStatus: 'ENABLED', validationStatus: 'VALID' },
+                        bulletins: [
+                            createTestBulletin(1, olderTimestamp), // Should be removed
+                            createTestBulletin(2, testTimestamp), // Should be kept (equal)
+                            createTestBulletin(3, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'flow-analysis-rule-1',
+                            name: 'Test Flow Analysis Rule',
+                            type: 'org.apache.nifi.TestFlowAnalysisRule',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'ENABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            enforcementPolicy: 'WARN'
+                        }
+                    },
+                    {
+                        id: 'flow-analysis-rule-2',
+                        uri: 'test-uri-2',
+                        status: { runStatus: 'DISABLED', validationStatus: 'VALID' },
+                        bulletins: [
+                            createTestBulletin(4, olderTimestamp) // Should be removed
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'flow-analysis-rule-2',
+                            name: 'Another Flow Analysis Rule',
+                            type: 'org.apache.nifi.AnotherFlowAnalysisRule',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'DISABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            enforcementPolicy: 'ENFORCE'
+                        }
+                    }
+                ]
+            };
+        });
+
+        it('should replace bulletins for the specified flow analysis rule', () => {
+            // Server returns bulletins that remain after clearing (equal and newer bulletins)
+            const action = clearFlowAnalysisRuleBulletinsSuccess({
+                response: {
+                    componentId: 'flow-analysis-rule-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.FlowAnalysisRule
+                }
+            });
+
+            const newState = flowAnalysisRulesReducer(testState, action);
+
+            const flowAnalysisRule = newState.flowAnalysisRules.find((far) => far.id === 'flow-analysis-rule-1');
+            expect(flowAnalysisRule).toBeDefined();
+            expect(flowAnalysisRule!.bulletins).toHaveLength(2);
+            expect(flowAnalysisRule!.bulletins!.map((b) => b.id)).toEqual([2, 3]); // Equal and newer bulletins remain
+        });
+
+        it('should not affect other flow analysis rules', () => {
+            const action = clearFlowAnalysisRuleBulletinsSuccess({
+                response: {
+                    componentId: 'flow-analysis-rule-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.FlowAnalysisRule
+                }
+            });
+
+            const newState = flowAnalysisRulesReducer(testState, action);
+
+            const otherFlowAnalysisRule = newState.flowAnalysisRules.find((far) => far.id === 'flow-analysis-rule-2');
+            expect(otherFlowAnalysisRule).toBeDefined();
+            expect(otherFlowAnalysisRule!.bulletins).toHaveLength(1); // Original count unchanged
+            expect(otherFlowAnalysisRule!.bulletins![0].id).toBe(4);
+        });
+
+        it('should handle flow analysis rule with no bulletins', () => {
+            testState.flowAnalysisRules[0].bulletins = [] as any;
+
+            const action = clearFlowAnalysisRuleBulletinsSuccess({
+                response: {
+                    componentId: 'flow-analysis-rule-1',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.FlowAnalysisRule
+                }
+            });
+
+            const newState = flowAnalysisRulesReducer(testState, action);
+
+            const flowAnalysisRule = newState.flowAnalysisRules.find((far) => far.id === 'flow-analysis-rule-1');
+            expect(flowAnalysisRule).toBeDefined();
+            expect(flowAnalysisRule!.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent flow analysis rule gracefully', () => {
+            const action = clearFlowAnalysisRuleBulletinsSuccess({
+                response: {
+                    componentId: 'non-existent-rule',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.FlowAnalysisRule
+                }
+            });
+
+            const newState = flowAnalysisRulesReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.ts
index 0ef9563918..e761815759 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.reducer.ts
@@ -18,6 +18,7 @@
 import { createReducer, on } from '@ngrx/store';
 import { FlowAnalysisRulesState } from './index';
 import {
+    clearFlowAnalysisRuleBulletinsSuccess,
     configureFlowAnalysisRule,
     configureFlowAnalysisRuleSuccess,
     createFlowAnalysisRule,
@@ -121,5 +122,17 @@ export const flowAnalysisRulesReducer = createReducer(
             }
             draftState.saving = false;
         });
+    }),
+    on(clearFlowAnalysisRuleBulletinsSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.flowAnalysisRules.findIndex(
+                (rule: any) => rule.id === response.componentId
+            );
+            if (componentIndex > -1) {
+                const rule = draftState.flowAnalysisRules[componentIndex];
+                // Replace bulletins with the current bulletins from the server
+                rule.bulletins = response.bulletins || [];
+            }
+        });
     })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.actions.ts
index f9a5e76528..4b4c7e6735 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.actions.ts
@@ -26,6 +26,8 @@ import {
     SelectControllerServiceRequest
 } from './index';
 import {
+    ClearBulletinsRequest,
+    ClearBulletinsResponse,
     CreateControllerServiceRequest,
     DisableControllerServiceDialogRequest,
     EditControllerServiceDialogRequest,
@@ -144,3 +146,13 @@ export const openChangeMgtControllerServiceVersionDialog = createAction(
     `[Management Controller Services] Open Change Management Controller Service Version Dialog`,
     props<{ request: FetchComponentVersionsRequest }>()
 );
+
+export const clearControllerServiceBulletins = createAction(
+    '[Management Controller Services] Clear Controller Service Bulletins',
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearControllerServiceBulletinsSuccess = createAction(
+    '[Management Controller Services] Clear Controller Service Bulletins Success',
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.spec.ts
index e052cdfca7..54d59e488c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,84 +17,124 @@
 
 import { TestBed } from '@angular/core/testing';
 import { provideMockActions } from '@ngrx/effects/testing';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockStore } from '@ngrx/store/testing';
 import { Action } from '@ngrx/store';
-import { ReplaySubject, of, take, throwError } from 'rxjs';
+import { ReplaySubject, of, throwError, firstValueFrom } from 'rxjs';
+import { take } from 'rxjs/operators';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
 import { HttpErrorResponse } from '@angular/common/http';
 
-import * as ManagementControllerServicesActions from './management-controller-services.actions';
 import { ManagementControllerServicesEffects } from './management-controller-services.effects';
+import * as ManagementControllerServicesActions from './management-controller-services.actions';
 import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
+import { Client } from '../../../../service/client.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
-import { Client } from '../../../../service/client.service';
 import { ExtensionTypesService } from '../../../../service/extension-types.service';
+import { ComponentType } from '@nifi/shared';
+import { managementControllerServicesFeatureKey } from './index';
 import { initialState } from './management-controller-services.reducer';
+import { settingsFeatureKey } from '../index';
 
 describe('ManagementControllerServicesEffects', () => {
-    interface SetupOptions {
-        managementControllerServicesState?: any;
-    }
+    let actions$: ReplaySubject<Action>;
+    let effects: ManagementControllerServicesEffects;
+    let mockManagementControllerServiceService: jest.Mocked<ManagementControllerServiceService>;
+    let mockErrorHelper: jest.Mocked<ErrorHelper>;
+
+    beforeEach(() => {
+        const managementControllerServiceServiceSpy = {
+            getControllerServices: jest.fn(),
+            createControllerService: jest.fn(),
+            getControllerService: jest.fn(),
+            getPropertyDescriptor: jest.fn(),
+            updateControllerService: jest.fn(),
+            deleteControllerService: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<ManagementControllerServiceService>;
 
-    const mockResponse = {
-        controllerServices: [],
-        currentTime: '2023-01-01 12:00:00 EST'
-    };
+        const errorHelperSpy = {
+            handleLoadingError: jest.fn().mockReturnValue({ type: '[Error] Loading Error' }),
+            getErrorString: jest.fn(),
+            showErrorInContext: jest.fn(),
+            fullScreenError: jest.fn()
+        } as unknown as jest.Mocked<ErrorHelper>;
 
-    async function setup({ managementControllerServicesState = initialState }: SetupOptions = {}) {
-        await TestBed.configureTestingModule({
+        TestBed.configureTestingModule({
             providers: [
                 ManagementControllerServicesEffects,
-                provideMockActions(() => action$),
+                provideMockActions(() => actions$),
                 provideMockStore({
                     initialState: {
-                        settings: {
-                            managementControllerServices: managementControllerServicesState
+                        [settingsFeatureKey]: {
+                            [managementControllerServicesFeatureKey]: initialState
                         }
                     }
                 }),
-                { provide: ManagementControllerServiceService, useValue: { getControllerServices: jest.fn() } },
-                { provide: ErrorHelper, useValue: { handleLoadingError: jest.fn() } },
-                { provide: MatDialog, useValue: { open: jest.fn(), closeAll: jest.fn() } },
-                { provide: Router, useValue: { navigate: jest.fn() } },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: Client, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} }
+                {
+                    provide: ManagementControllerServiceService,
+                    useValue: managementControllerServiceServiceSpy
+                },
+                {
+                    provide: Client,
+                    useValue: {
+                        getClientId: jest.fn().mockReturnValue('test-client-id')
+                    }
+                },
+                {
+                    provide: ErrorHelper,
+                    useValue: errorHelperSpy
+                },
+                {
+                    provide: Router,
+                    useValue: {
+                        navigate: jest.fn()
+                    }
+                },
+                {
+                    provide: MatDialog,
+                    useValue: {
+                        open: jest.fn(),
+                        closeAll: jest.fn()
+                    }
+                },
+                {
+                    provide: PropertyTableHelperService,
+                    useValue: {
+                        getComponentHistory: jest.fn(),
+                        createNewProperty: jest.fn(),
+                        createNewService: jest.fn()
+                    }
+                },
+                {
+                    provide: ExtensionTypesService,
+                    useValue: {
+                        getControllerServiceVersionsForType: jest.fn()
+                    }
+                }
             ]
-        }).compileComponents();
-
-        const store = TestBed.inject(MockStore);
-        const effects = TestBed.inject(ManagementControllerServicesEffects);
-        const managementControllerServiceService = TestBed.inject(ManagementControllerServiceService);
-        const errorHelper = TestBed.inject(ErrorHelper);
-
-        return { effects, store, managementControllerServiceService, errorHelper };
-    }
-
-    let action$: ReplaySubject<Action>;
-    beforeEach(() => {
-        action$ = new ReplaySubject<Action>();
-    });
+        });
 
-    it('should create', async () => {
-        const { effects } = await setup();
-        expect(effects).toBeTruthy();
+        effects = TestBed.inject(ManagementControllerServicesEffects);
+        mockManagementControllerServiceService = TestBed.inject(
+            ManagementControllerServiceService
+        ) as jest.Mocked<ManagementControllerServiceService>;
+        mockErrorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        actions$ = new ReplaySubject(1);
     });
 
-    describe('Load Management Controller Services', () => {
-        it('should load management controller services successfully', async () => {
-            const { effects, managementControllerServiceService } = await setup();
+    describe('loadManagementControllerServices$', () => {
+        it('should dispatch loadManagementControllerServicesSuccess when services are loaded successfully', async () => {
+            const mockResponse = {
+                controllerServices: [],
+                currentTime: '2023-01-01 12:00:00 EST'
+            };
+            mockManagementControllerServiceService.getControllerServices.mockReturnValue(of(mockResponse));
 
-            action$.next(ManagementControllerServicesActions.loadManagementControllerServices());
-            jest.spyOn(managementControllerServiceService, 'getControllerServices').mockReturnValueOnce(
-                of(mockResponse) as never
-            );
+            actions$.next(ManagementControllerServicesActions.loadManagementControllerServices());
 
-            const result = await new Promise((resolve) =>
-                effects.loadManagementControllerServices$.pipe(take(1)).subscribe(resolve)
-            );
+            const result = await firstValueFrom(effects.loadManagementControllerServices$);
 
             expect(result).toEqual(
                 ManagementControllerServicesActions.loadManagementControllerServicesSuccess({
@@ -104,103 +144,175 @@ describe('ManagementControllerServicesEffects', () => {
                     }
                 })
             );
+            expect(mockManagementControllerServiceService.getControllerServices).toHaveBeenCalled();
         });
 
-        it('should fail to load management controller services on initial load with hasExistingData=false', async () => {
-            const { effects, managementControllerServiceService } = await setup();
-
-            action$.next(ManagementControllerServicesActions.loadManagementControllerServices());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(managementControllerServiceService, 'getControllerServices').mockImplementationOnce(() =>
-                throwError(() => error)
+        it('should dispatch loadManagementControllerServicesError with status pending on initial load failure', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockManagementControllerServiceService.getControllerServices.mockReturnValue(
+                throwError(() => errorResponse)
             );
 
-            const result = await new Promise((resolve) =>
-                effects.loadManagementControllerServices$.pipe(take(1)).subscribe(resolve)
-            );
+            actions$.next(ManagementControllerServicesActions.loadManagementControllerServices());
+
+            const result = await firstValueFrom(effects.loadManagementControllerServices$);
 
             expect(result).toEqual(
                 ManagementControllerServicesActions.loadManagementControllerServicesError({
-                    errorResponse: error,
+                    errorResponse,
                     loadedTimestamp: initialState.loadedTimestamp,
                     status: 'pending'
                 })
             );
         });
-
-        it('should fail to load management controller services on refresh with hasExistingData=true', async () => {
-            const stateWithData = { ...initialState, loadedTimestamp: '2023-01-01 11:00:00 EST' };
-            const { effects, managementControllerServiceService } = await setup({
-                managementControllerServicesState: stateWithData
-            });
-
-            action$.next(ManagementControllerServicesActions.loadManagementControllerServices());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(managementControllerServiceService, 'getControllerServices').mockImplementationOnce(() =>
-                throwError(() => error)
-            );
-
-            const result = await new Promise((resolve) =>
-                effects.loadManagementControllerServices$.pipe(take(1)).subscribe(resolve)
-            );
-
-            expect(result).toEqual(
-                ManagementControllerServicesActions.loadManagementControllerServicesError({
-                    errorResponse: error,
-                    loadedTimestamp: stateWithData.loadedTimestamp,
-                    status: 'success'
-                })
-            );
-        });
     });
 
-    describe('Load Management Controller Services Error', () => {
-        it('should handle management controller services error for initial load', async () => {
-            const { effects, errorHelper } = await setup();
-
-            const error = new HttpErrorResponse({ status: 500 });
+    describe('loadManagementControllerServicesError$', () => {
+        it('should handle error on initial load (hasExistingData=false)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
             const errorAction = ManagementControllerServicesActions.managementControllerServicesBannerApiError({
-                error: 'Error message'
+                error: 'Error loading'
             });
-            action$.next(
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
                 ManagementControllerServicesActions.loadManagementControllerServicesError({
-                    errorResponse: error,
+                    errorResponse,
                     loadedTimestamp: initialState.loadedTimestamp,
                     status: 'pending'
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
 
-            const result = await new Promise((resolve) =>
-                effects.loadManagementControllerServicesError$.pipe(take(1)).subscribe(resolve)
-            );
+            const result = await firstValueFrom(effects.loadManagementControllerServicesError$);
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(false, error);
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(false, errorResponse);
             expect(result).toEqual(errorAction);
         });
 
-        it('should handle management controller services error for refresh', async () => {
-            const { effects, errorHelper } = await setup();
-
-            const error = new HttpErrorResponse({ status: 500 });
+        it('should handle error on refresh (hasExistingData=true)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
             const errorAction = ManagementControllerServicesActions.managementControllerServicesBannerApiError({
-                error: 'Error message'
+                error: 'Error loading'
             });
-            action$.next(
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
                 ManagementControllerServicesActions.loadManagementControllerServicesError({
-                    errorResponse: error,
+                    errorResponse,
                     loadedTimestamp: '2023-01-01 11:00:00 EST',
                     status: 'success'
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
 
-            const result = await new Promise((resolve) =>
-                effects.loadManagementControllerServicesError$.pipe(take(1)).subscribe(resolve)
-            );
+            const result = await firstValueFrom(effects.loadManagementControllerServicesError$);
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(true, error);
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(true, errorResponse);
             expect(result).toEqual(errorAction);
         });
     });
+
+    describe('clearControllerServiceBulletins$', () => {
+        it('should dispatch clearControllerServiceBulletinsSuccess action when bulletins are cleared successfully', () => {
+            const request = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'controller-service-1',
+                componentType: ComponentType.ControllerService
+            };
+
+            const mockResponse = {
+                bulletinsCleared: 3,
+                bulletins: []
+            };
+
+            mockManagementControllerServiceService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            const action = ManagementControllerServicesActions.clearControllerServiceBulletins({ request });
+            actions$.next(action);
+
+            effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe((result) => {
+                expect(result).toEqual(
+                    ManagementControllerServicesActions.clearControllerServiceBulletinsSuccess({
+                        response: {
+                            componentId: request.componentId,
+                            bulletinsCleared: mockResponse.bulletinsCleared,
+                            bulletins: mockResponse.bulletins || [],
+                            componentType: request.componentType
+                        }
+                    })
+                );
+            });
+
+            expect(mockManagementControllerServiceService.clearBulletins).toHaveBeenCalledWith({
+                uri: request.uri,
+                fromTimestamp: request.fromTimestamp
+            });
+        });
+
+        it('should handle bulletinsCleared being undefined and default to 0', () => {
+            const request = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'controller-service-2',
+                componentType: ComponentType.ControllerService
+            };
+
+            const mockResponse = {
+                // bulletinsCleared is undefined
+                bulletins: []
+            };
+
+            mockManagementControllerServiceService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            const action = ManagementControllerServicesActions.clearControllerServiceBulletins({ request });
+            actions$.next(action);
+
+            effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe((result) => {
+                expect(result).toEqual(
+                    ManagementControllerServicesActions.clearControllerServiceBulletinsSuccess({
+                        response: {
+                            componentId: request.componentId,
+                            bulletinsCleared: 0, // Should default to 0
+                            bulletins: mockResponse.bulletins || [],
+                            componentType: request.componentType
+                        }
+                    })
+                );
+            });
+        });
+
+        it('should preserve request fields in the success response', () => {
+            const request = {
+                uri: 'different-uri',
+                fromTimestamp: '2023-06-15T08:30:00.000Z',
+                componentId: 'another-controller-service',
+                componentType: ComponentType.ControllerService
+            };
+
+            const mockResponse = {
+                bulletinsCleared: 7,
+                bulletins: []
+            };
+
+            mockManagementControllerServiceService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            const action = ManagementControllerServicesActions.clearControllerServiceBulletins({ request });
+            actions$.next(action);
+
+            effects.clearControllerServiceBulletins$.pipe(take(1)).subscribe((result) => {
+                expect(result).toEqual(
+                    ManagementControllerServicesActions.clearControllerServiceBulletinsSuccess({
+                        response: {
+                            componentId: request.componentId, // From request
+                            bulletinsCleared: mockResponse.bulletinsCleared, // From response
+                            bulletins: mockResponse.bulletins || [], // From response
+                            componentType: request.componentType // From request
+                        }
+                    })
+                );
+            });
+        });
+    });
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts
index a141aec27c..b2598f43f5 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts
@@ -700,6 +700,39 @@ export class ManagementControllerServicesEffects {
         { dispatch: false }
     );
 
+    clearControllerServiceBulletins$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ManagementControllerServicesActions.clearControllerServiceBulletins),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.managementControllerServiceService.clearBulletins({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    })
+                ).pipe(
+                    map((response) =>
+                        ManagementControllerServicesActions.clearControllerServiceBulletinsSuccess({
+                            response: {
+                                componentId: request.componentId,
+                                bulletinsCleared: response.bulletinsCleared || 0,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            ManagementControllerServicesActions.managementControllerServicesSnackbarApiError({
+                                error: this.errorHelper.getErrorString(errorResponse)
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
+
     private getRouteForReference(reference: ControllerServiceReferencingComponent): string[] {
         if (reference.referenceType == 'ControllerService') {
             if (reference.groupId == null) {
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.spec.ts
new file mode 100644
index 0000000000..ad1b974028
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.spec.ts
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { managementControllerServicesReducer, initialState } from './management-controller-services.reducer';
+import { clearControllerServiceBulletinsSuccess } from './management-controller-services.actions';
+import { ManagementControllerServicesState } from './index';
+import { BulletinEntity, ComponentType } from '@nifi/shared';
+
+describe('Management Controller Services Reducer', () => {
+    describe('clearControllerServiceBulletinsSuccess', () => {
+        let testState: ManagementControllerServicesState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'source1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'source1',
+                    sourceName: 'Test Source',
+                    sourceType: 'COMPONENT',
+                    level: 'ERROR',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialState,
+                controllerServices: [
+                    {
+                        id: 'controller-service-1',
+                        uri: 'test-uri-1',
+                        status: { runStatus: 'ENABLED', validationStatus: 'VALID' },
+                        bulletins: [
+                            createTestBulletin(1, olderTimestamp), // Should be removed
+                            createTestBulletin(2, testTimestamp), // Should be kept (equal)
+                            createTestBulletin(3, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'controller-service-1',
+                            name: 'Test Controller Service',
+                            type: 'org.apache.nifi.TestControllerService',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'ENABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: []
+                        }
+                    },
+                    {
+                        id: 'controller-service-2',
+                        uri: 'test-uri-2',
+                        status: { runStatus: 'DISABLED', validationStatus: 'VALID' },
+                        bulletins: [
+                            createTestBulletin(4, olderTimestamp) // Should be removed
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'controller-service-2',
+                            name: 'Another Controller Service',
+                            type: 'org.apache.nifi.AnotherControllerService',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            state: 'DISABLED',
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: []
+                        }
+                    }
+                ]
+            };
+        });
+
+        it('should replace bulletins for the specified controller service', () => {
+            // Server returns bulletins that remain after clearing (equal and newer bulletins)
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            const controllerService = newState.controllerServices.find((cs) => cs.id === 'controller-service-1');
+            expect(controllerService).toBeDefined();
+            expect(controllerService!.bulletins).toHaveLength(2);
+            expect(controllerService!.bulletins!.map((b) => b.id)).toEqual([2, 3]); // Equal and newer bulletins remain
+        });
+
+        it('should not affect other controller services', () => {
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            const otherControllerService = newState.controllerServices.find((cs) => cs.id === 'controller-service-2');
+            expect(otherControllerService).toBeDefined();
+            expect(otherControllerService!.bulletins).toHaveLength(1); // Original count unchanged
+            expect(otherControllerService!.bulletins![0].id).toBe(4);
+        });
+
+        it('should handle controller service with no bulletins', () => {
+            testState.controllerServices[0].bulletins = [] as any;
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            const controllerService = newState.controllerServices.find((cs) => cs.id === 'controller-service-1');
+            expect(controllerService).toBeDefined();
+            expect(controllerService!.bulletins).toEqual([]);
+        });
+
+        it('should handle controller service with empty bulletins array', () => {
+            testState.controllerServices[0].bulletins = [];
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-1',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            const controllerService = newState.controllerServices.find((cs) => cs.id === 'controller-service-1');
+            expect(controllerService).toBeDefined();
+            expect(controllerService!.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent controller service gracefully', () => {
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'non-existent-service',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+
+        it('should remove all bulletins older than the specified timestamp', () => {
+            testState.controllerServices[1].bulletins = [
+                createTestBulletin(5, olderTimestamp),
+                createTestBulletin(6, new Date('2023-01-01T10:00:00.000Z').getTime()), // Even older
+                createTestBulletin(7, newerTimestamp)
+            ];
+
+            const action = clearControllerServiceBulletinsSuccess({
+                response: {
+                    componentId: 'controller-service-2',
+                    bulletinsCleared: 2,
+                    bulletins: [createTestBulletin(7, newerTimestamp)], // Only newer bulletin remains
+                    componentType: ComponentType.ControllerService
+                }
+            });
+
+            const newState = managementControllerServicesReducer(testState, action);
+
+            const controllerService = newState.controllerServices.find((cs) => cs.id === 'controller-service-2');
+            expect(controllerService).toBeDefined();
+            expect(controllerService!.bulletins).toHaveLength(1);
+            expect(controllerService!.bulletins![0].id).toBe(7); // Only newer bulletin remains
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.ts
index 4915195286..aba59f3320 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.reducer.ts
@@ -18,6 +18,7 @@
 import { createReducer, on } from '@ngrx/store';
 import { ManagementControllerServicesState } from './index';
 import {
+    clearControllerServiceBulletinsSuccess,
     configureControllerService,
     configureControllerServiceSuccess,
     createControllerService,
@@ -98,5 +99,17 @@ export const managementControllerServicesReducer = createReducer(
             }
             draftState.saving = false;
         });
+    }),
+    on(clearControllerServiceBulletinsSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.controllerServices.findIndex(
+                (service: any) => service.id === response.componentId
+            );
+            if (componentIndex > -1) {
+                const service = draftState.controllerServices[componentIndex];
+                // Replace bulletins with the current bulletins from the server
+                service.bulletins = response.bulletins || [];
+            }
+        });
     })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/index.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/index.ts
index 3e51c2a08b..6b6679d583 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/index.ts
@@ -16,7 +16,13 @@
  */
 
 import { Bundle, ComponentHistory, ParameterEntity, PropertyDescriptor } from '../../../../state/shared';
-import { AffectedComponentEntity, ParameterContextReferenceEntity, Permissions, Revision } from '@nifi/shared';
+import {
+    AffectedComponentEntity,
+    BulletinEntity,
+    ParameterContextReferenceEntity,
+    Permissions,
+    Revision
+} from '@nifi/shared';
 
 export const parameterProvidersFeatureKey = 'parameterProviders';
 
@@ -67,7 +73,7 @@ export interface ParameterProvider {
 
 export interface ParameterProviderEntity {
     id: string;
-    bulletins: [];
+    bulletins: BulletinEntity[];
     component: ParameterProvider;
     permissions: Permissions;
     revision: Revision;
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.actions.ts
index 5652317803..24c17e2fb3 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.actions.ts
@@ -31,6 +31,7 @@ import {
     PollParameterProviderParametersUpdateSuccess,
     SelectParameterProviderRequest
 } from './index';
+import { ClearBulletinsRequest, ClearBulletinsResponse } from '../../../../state/shared';
 
 const PARAMETER_PROVIDERS_PREFIX = '[Parameter Providers]';
 
@@ -176,3 +177,13 @@ export const deleteParameterProviderParametersUpdateRequest = createAction(
 export const submitParameterProviderParametersUpdateComplete = createAction(
     `${PARAMETER_PROVIDERS_PREFIX} Submit Parameter Provider Parameters Update Complete`
 );
+
+export const clearParameterProviderBulletins = createAction(
+    `${PARAMETER_PROVIDERS_PREFIX} Clear Parameter Provider Bulletins`,
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearParameterProviderBulletinsSuccess = createAction(
+    `${PARAMETER_PROVIDERS_PREFIX} Clear Parameter Provider Bulletins Success`,
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.spec.ts
index ea95c21a85..c75dfd7862 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,88 +17,180 @@
 
 import { TestBed } from '@angular/core/testing';
 import { provideMockActions } from '@ngrx/effects/testing';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockStore } from '@ngrx/store/testing';
 import { Action } from '@ngrx/store';
-import { ReplaySubject, of, take, throwError } from 'rxjs';
+import { ReplaySubject, of, throwError, firstValueFrom } from 'rxjs';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
 import { HttpErrorResponse } from '@angular/common/http';
 
-import * as ParameterProviderActions from './parameter-providers.actions';
 import { ParameterProvidersEffects } from './parameter-providers.effects';
+import * as ParameterProviderActions from './parameter-providers.actions';
 import { ParameterProviderService } from '../../service/parameter-provider.service';
-import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
+import { Client } from '../../../../service/client.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
-import { Client } from '../../../../service/client.service';
-import { ExtensionTypesService } from '../../../../service/extension-types.service';
+import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
+import { ComponentType } from '@nifi/shared';
+import { ParameterProviderEntity, parameterProvidersFeatureKey } from './index';
 import { initialParameterProvidersState } from './parameter-providers.reducer';
+import { settingsFeatureKey } from '../index';
 
 describe('ParameterProvidersEffects', () => {
-    interface SetupOptions {
-        parameterProvidersState?: any;
+    let actions$: ReplaySubject<Action>;
+    let effects: ParameterProvidersEffects;
+    let mockParameterProviderService: jest.Mocked<ParameterProviderService>;
+    let mockErrorHelper: jest.Mocked<ErrorHelper>;
+    let mockDialog: jest.Mocked<MatDialog>;
+
+    // Mock data factories
+    function createMockParameterProviderEntity(): ParameterProviderEntity {
+        return {
+            id: 'test-parameter-provider-id',
+            uri: 'test-uri',
+            permissions: {
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 1,
+                clientId: 'test-client'
+            },
+            component: {
+                id: 'test-parameter-provider-id',
+                name: 'Test Parameter Provider',
+                type: 'org.apache.nifi.TestParameterProvider',
+                bundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                properties: {},
+                descriptors: {},
+                validationErrors: [],
+                affectedComponents: [],
+                parameterGroupConfigurations: [],
+                comments: '',
+                deprecated: false,
+                extensionMissing: false,
+                multipleVersionsAvailable: false,
+                persistsState: false,
+                referencingParameterContexts: [],
+                restricted: false,
+                validationStatus: 'VALID'
+            },
+            bulletins: []
+        } as ParameterProviderEntity;
     }
 
-    const mockResponse = {
-        parameterProviders: [],
-        currentTime: '2023-01-01 12:00:00 EST'
-    };
+    beforeEach(() => {
+        const parameterProviderServiceSpy = {
+            getParameterProviders: jest.fn(),
+            createParameterProvider: jest.fn(),
+            updateParameterProvider: jest.fn(),
+            deleteParameterProvider: jest.fn(),
+            fetchParameters: jest.fn(),
+            applyParameters: jest.fn(),
+            pollParameterProviderParametersUpdateRequest: jest.fn(),
+            deleteParameterProviderParametersUpdateRequest: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<ParameterProviderService>;
+
+        const clientSpy = {
+            getClientId: jest.fn().mockReturnValue('test-client-id')
+        } as unknown as jest.Mocked<Client>;
+
+        const errorHelperSpy = {
+            handleLoadingError: jest.fn().mockReturnValue({ type: '[Error] Loading Error' }),
+            getErrorString: jest.fn().mockReturnValue('Test error message'),
+            showErrorInContext: jest.fn().mockReturnValue(true),
+            fullScreenError: jest.fn()
+        } as unknown as jest.Mocked<ErrorHelper>;
+
+        const routerSpy = {
+            navigate: jest.fn()
+        } as unknown as jest.Mocked<Router>;
 
-    async function setup({ parameterProvidersState = initialParameterProvidersState }: SetupOptions = {}) {
-        await TestBed.configureTestingModule({
+        const dialogSpy = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        } as unknown as jest.Mocked<MatDialog>;
+
+        TestBed.configureTestingModule({
             providers: [
                 ParameterProvidersEffects,
-                provideMockActions(() => action$),
+                provideMockActions(() => actions$),
                 provideMockStore({
                     initialState: {
-                        settings: {
-                            parameterProviders: parameterProvidersState
+                        [settingsFeatureKey]: {
+                            [parameterProvidersFeatureKey]: initialParameterProvidersState
                         }
                     }
                 }),
-                { provide: ParameterProviderService, useValue: { getParameterProviders: jest.fn() } },
-                { provide: ManagementControllerServiceService, useValue: {} },
-                { provide: ErrorHelper, useValue: { handleLoadingError: jest.fn() } },
-                { provide: MatDialog, useValue: { open: jest.fn(), closeAll: jest.fn() } },
-                { provide: Router, useValue: { navigate: jest.fn() } },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: Client, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} }
+                {
+                    provide: ParameterProviderService,
+                    useValue: parameterProviderServiceSpy
+                },
+                {
+                    provide: Client,
+                    useValue: clientSpy
+                },
+                {
+                    provide: ErrorHelper,
+                    useValue: errorHelperSpy
+                },
+                {
+                    provide: Router,
+                    useValue: routerSpy
+                },
+                {
+                    provide: MatDialog,
+                    useValue: dialogSpy
+                },
+                {
+                    provide: PropertyTableHelperService,
+                    useValue: {
+                        getComponentHistory: jest.fn(),
+                        createNewProperty: jest.fn(),
+                        createNewService: jest.fn()
+                    }
+                },
+                {
+                    provide: ManagementControllerServiceService,
+                    useValue: {
+                        getControllerServices: jest.fn(),
+                        createControllerService: jest.fn(),
+                        getControllerService: jest.fn(),
+                        updateControllerService: jest.fn(),
+                        deleteControllerService: jest.fn()
+                    }
+                }
             ]
-        }).compileComponents();
-
-        const store = TestBed.inject(MockStore);
-        const effects = TestBed.inject(ParameterProvidersEffects);
-        const parameterProviderService = TestBed.inject(ParameterProviderService);
-        const errorHelper = TestBed.inject(ErrorHelper);
-
-        return { effects, store, parameterProviderService, errorHelper };
-    }
+        });
 
-    let action$: ReplaySubject<Action>;
-    beforeEach(() => {
-        action$ = new ReplaySubject<Action>();
+        effects = TestBed.inject(ParameterProvidersEffects);
+        mockParameterProviderService = TestBed.inject(
+            ParameterProviderService
+        ) as jest.Mocked<ParameterProviderService>;
+        mockErrorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        mockDialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        actions$ = new ReplaySubject(1);
     });
 
-    it('should create', async () => {
-        const { effects } = await setup();
-        expect(effects).toBeTruthy();
-    });
+    describe('loadParameterProviders$', () => {
+        it('should dispatch loadParameterProvidersSuccess action when parameter providers are loaded successfully', async () => {
+            const mockResponse = {
+                parameterProviders: [createMockParameterProviderEntity()],
+                currentTime: '2023-01-01 12:00:00 EST'
+            };
 
-    describe('Load Parameter Providers', () => {
-        it('should load parameter providers successfully', async () => {
-            const { effects, parameterProviderService } = await setup();
+            mockParameterProviderService.getParameterProviders.mockReturnValue(of(mockResponse));
 
-            action$.next(ParameterProviderActions.loadParameterProviders());
-            jest.spyOn(parameterProviderService, 'getParameterProviders').mockReturnValueOnce(
-                of(mockResponse) as never
-            );
+            actions$.next(ParameterProviderActions.loadParameterProviders());
 
-            const result = await new Promise((resolve) =>
-                effects.loadParameterProviders$.pipe(take(1)).subscribe(resolve)
-            );
+            const action = await firstValueFrom(effects.loadParameterProviders$);
 
-            expect(result).toEqual(
+            expect(action).toEqual(
                 ParameterProviderActions.loadParameterProvidersSuccess({
                     response: {
                         parameterProviders: mockResponse.parameterProviders,
@@ -106,99 +198,416 @@ describe('ParameterProvidersEffects', () => {
                     }
                 })
             );
+            expect(mockParameterProviderService.getParameterProviders).toHaveBeenCalled();
         });
 
-        it('should fail to load parameter providers on initial load with hasExistingData=false', async () => {
-            const { effects, parameterProviderService } = await setup();
+        it('should dispatch loadParameterProvidersError with status pending on initial load failure', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockParameterProviderService.getParameterProviders.mockReturnValue(throwError(() => errorResponse));
 
-            action$.next(ParameterProviderActions.loadParameterProviders());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(parameterProviderService, 'getParameterProviders').mockImplementationOnce(() =>
-                throwError(() => error)
-            );
+            actions$.next(ParameterProviderActions.loadParameterProviders());
 
-            const result = await new Promise((resolve) =>
-                effects.loadParameterProviders$.pipe(take(1)).subscribe(resolve)
-            );
+            const action = await firstValueFrom(effects.loadParameterProviders$);
 
-            expect(result).toEqual(
+            expect(action).toEqual(
                 ParameterProviderActions.loadParameterProvidersError({
-                    errorResponse: error,
+                    errorResponse,
                     loadedTimestamp: initialParameterProvidersState.loadedTimestamp,
                     status: 'pending'
                 })
             );
         });
+    });
 
-        it('should fail to load parameter providers on refresh with hasExistingData=true', async () => {
-            const stateWithData = { ...initialParameterProvidersState, loadedTimestamp: '2023-01-01 11:00:00 EST' };
-            const { effects, parameterProviderService } = await setup({
-                parameterProvidersState: stateWithData
-            });
+    describe('loadParameterProvidersError$', () => {
+        it('should handle error on initial load (hasExistingData=false)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = ParameterProviderActions.parameterProvidersBannerApiError({ error: 'Error loading' });
 
-            action$.next(ParameterProviderActions.loadParameterProviders());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(parameterProviderService, 'getParameterProviders').mockImplementationOnce(() =>
-                throwError(() => error)
-            );
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
 
-            const result = await new Promise((resolve) =>
-                effects.loadParameterProviders$.pipe(take(1)).subscribe(resolve)
+            actions$.next(
+                ParameterProviderActions.loadParameterProvidersError({
+                    errorResponse,
+                    loadedTimestamp: initialParameterProvidersState.loadedTimestamp,
+                    status: 'pending'
+                })
             );
 
-            expect(result).toEqual(
+            const action = await firstValueFrom(effects.loadParameterProvidersError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(false, errorResponse);
+            expect(action).toEqual(errorAction);
+        });
+
+        it('should handle error on refresh (hasExistingData=true)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = ParameterProviderActions.parameterProvidersBannerApiError({ error: 'Error loading' });
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
                 ParameterProviderActions.loadParameterProvidersError({
-                    errorResponse: error,
-                    loadedTimestamp: stateWithData.loadedTimestamp,
+                    errorResponse,
+                    loadedTimestamp: '2023-01-01 11:00:00 EST',
                     status: 'success'
                 })
             );
+
+            const action = await firstValueFrom(effects.loadParameterProvidersError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(true, errorResponse);
+            expect(action).toEqual(errorAction);
         });
     });
 
-    describe('loadParameterProvidersError$', () => {
-        it('should handle parameter providers error for initial load', async () => {
-            const { effects, errorHelper } = await setup();
+    describe('createParameterProvider$', () => {
+        it('should dispatch createParameterProviderSuccess action when parameter provider is created successfully', async () => {
+            const mockRequest = {
+                parameterProviderType: 'org.apache.nifi.TestParameterProvider',
+                parameterProviderBundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                revision: {
+                    clientId: 'test-client-id',
+                    version: 0
+                }
+            };
+            const mockResponse = createMockParameterProviderEntity();
 
-            const error = new HttpErrorResponse({ status: 500 });
-            const errorAction = ParameterProviderActions.parameterProvidersBannerApiError({ error: 'Error message' });
-            action$.next(
-                ParameterProviderActions.loadParameterProvidersError({
-                    errorResponse: error,
-                    loadedTimestamp: initialParameterProvidersState.loadedTimestamp,
-                    status: 'pending'
+            mockParameterProviderService.createParameterProvider.mockReturnValue(of(mockResponse));
+
+            actions$.next(ParameterProviderActions.createParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.createParameterProvider$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.createParameterProviderSuccess({
+                    response: {
+                        parameterProvider: mockResponse
+                    }
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+            expect(mockParameterProviderService.createParameterProvider).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error and close dialog when creating parameter provider fails', async () => {
+            const mockRequest = {
+                parameterProviderType: 'org.apache.nifi.TestParameterProvider',
+                parameterProviderBundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                revision: {
+                    clientId: 'test-client-id',
+                    version: 0
+                }
+            };
+            const mockError = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
 
-            const result = await new Promise((resolve) =>
-                effects.loadParameterProvidersError$.pipe(take(1)).subscribe(resolve)
+            mockParameterProviderService.createParameterProvider.mockReturnValue(throwError(() => mockError));
+
+            actions$.next(ParameterProviderActions.createParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.createParameterProvider$);
+
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(mockError);
+            expect(action.type).toContain('Snackbar Error');
+        });
+    });
+
+    describe('deleteParameterProvider$', () => {
+        it('should dispatch deleteParameterProviderSuccess action when parameter provider is deleted successfully', async () => {
+            const mockParameterProvider = createMockParameterProviderEntity();
+            const mockRequest = { parameterProvider: mockParameterProvider };
+
+            mockParameterProviderService.deleteParameterProvider.mockReturnValue(of(mockParameterProvider));
+
+            actions$.next(ParameterProviderActions.deleteParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.deleteParameterProvider);
+
+            expect(action).toEqual(
+                ParameterProviderActions.deleteParameterProviderSuccess({
+                    response: {
+                        parameterProvider: mockParameterProvider
+                    }
+                })
             );
+            expect(mockParameterProviderService.deleteParameterProvider).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error when deleting parameter provider fails', async () => {
+            const mockParameterProvider = createMockParameterProviderEntity();
+            const mockRequest = { parameterProvider: mockParameterProvider };
+            const mockError = new HttpErrorResponse({ status: 404, statusText: 'Not Found' });
+
+            mockParameterProviderService.deleteParameterProvider.mockReturnValue(throwError(() => mockError));
+
+            actions$.next(ParameterProviderActions.deleteParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.deleteParameterProvider);
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(false, error);
-            expect(result).toEqual(errorAction);
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(mockError);
+            expect(action.type).toContain('Snackbar Error');
         });
+    });
 
-        it('should handle parameter providers error for refresh', async () => {
-            const { effects, errorHelper } = await setup();
+    describe('clearParameterProviderBulletins$', () => {
+        it('should dispatch clearParameterProviderBulletinsSuccess action when bulletins are cleared successfully', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-parameter-provider-id',
+                componentType: ComponentType.ParameterProvider
+            };
+            const mockResponse = {
+                bulletinsCleared: 5,
+                bulletins: []
+            };
 
-            const error = new HttpErrorResponse({ status: 500 });
-            const errorAction = ParameterProviderActions.parameterProvidersBannerApiError({ error: 'Error message' });
-            action$.next(
-                ParameterProviderActions.loadParameterProvidersError({
-                    errorResponse: error,
-                    loadedTimestamp: '2023-01-01 11:00:00 EST',
-                    status: 'success'
+            mockParameterProviderService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(ParameterProviderActions.clearParameterProviderBulletins({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.clearParameterProviderBulletins$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.clearParameterProviderBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: mockResponse.bulletinsCleared,
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
+                })
+            );
+            expect(mockParameterProviderService.clearBulletins).toHaveBeenCalledWith({
+                uri: mockRequest.uri,
+                fromTimestamp: mockRequest.fromTimestamp
+            });
+        });
+
+        it('should handle undefined bulletinsCleared response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-parameter-provider-id',
+                componentType: ComponentType.ParameterProvider
+            };
+            const mockResponse = {
+                bulletins: []
+            }; // No bulletinsCleared property
+
+            mockParameterProviderService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(ParameterProviderActions.clearParameterProviderBulletins({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.clearParameterProviderBulletins$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.clearParameterProviderBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: 0, // Should default to 0
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
+                })
+            );
+        });
+
+        it('should pass through all required fields in the success response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-parameter-provider-id',
+                componentType: ComponentType.ParameterProvider
+            };
+            const mockResponse = {
+                bulletinsCleared: 3,
+                bulletins: []
+            };
+
+            mockParameterProviderService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(ParameterProviderActions.clearParameterProviderBulletins({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.clearParameterProviderBulletins$);
+
+            const response = (action as any).response;
+            expect(response.componentId).toBe(mockRequest.componentId);
+            expect(response.bulletinsCleared).toBe(mockResponse.bulletinsCleared);
+            expect(response.componentType).toBe(mockRequest.componentType);
+            expect(response.bulletins).toEqual([]);
+        });
+    });
+
+    describe('fetchParameterProviderParametersAndOpenDialog$', () => {
+        it('should dispatch fetchParameterProviderParametersSuccess action when parameters are fetched successfully', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                revision: {
+                    version: 1,
+                    clientId: 'test-client-id'
+                }
+            };
+            const mockResponse = createMockParameterProviderEntity();
+
+            mockParameterProviderService.fetchParameters.mockReturnValue(of(mockResponse));
+
+            actions$.next(
+                ParameterProviderActions.fetchParameterProviderParametersAndOpenDialog({ request: mockRequest })
+            );
+
+            const action = await firstValueFrom(effects.fetchParameterProviderParametersAndOpenDialog$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.fetchParameterProviderParametersSuccess({
+                    response: { parameterProvider: mockResponse }
                 })
             );
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+            expect(mockParameterProviderService.fetchParameters).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error when fetching parameters fails', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                revision: {
+                    version: 1,
+                    clientId: 'test-client-id'
+                }
+            };
+            const mockError = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+
+            mockParameterProviderService.fetchParameters.mockReturnValue(throwError(() => mockError));
+            mockErrorHelper.showErrorInContext.mockReturnValue(true);
+
+            actions$.next(
+                ParameterProviderActions.fetchParameterProviderParametersAndOpenDialog({ request: mockRequest })
+            );
+
+            const action = await firstValueFrom(effects.fetchParameterProviderParametersAndOpenDialog$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(mockError);
+            expect(action.type).toContain('Snackbar Error');
+        });
+
+        it('should handle full screen error when showErrorInContext returns false', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                revision: {
+                    version: 1,
+                    clientId: 'test-client-id'
+                }
+            };
+            const mockError = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            const mockFullScreenErrorAction = { type: 'FULL_SCREEN_ERROR' };
+
+            mockParameterProviderService.fetchParameters.mockReturnValue(throwError(() => mockError));
+            mockErrorHelper.showErrorInContext.mockReturnValue(false);
+            mockErrorHelper.fullScreenError.mockReturnValue(mockFullScreenErrorAction);
+
+            actions$.next(
+                ParameterProviderActions.fetchParameterProviderParametersAndOpenDialog({ request: mockRequest })
+            );
+
+            const action = await firstValueFrom(effects.fetchParameterProviderParametersAndOpenDialog$);
+
+            expect(action).toEqual(mockFullScreenErrorAction);
+            expect(mockErrorHelper.fullScreenError).toHaveBeenCalledWith(mockError);
+        });
+    });
+
+    describe('configureParameterProvider$', () => {
+        it('should dispatch configureParameterProviderSuccess action when parameter provider is configured successfully', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client-id' },
+                    component: { name: 'Updated Parameter Provider' }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const mockResponse = createMockParameterProviderEntity();
+
+            mockParameterProviderService.updateParameterProvider.mockReturnValue(of(mockResponse));
 
-            const result = await new Promise((resolve) =>
-                effects.loadParameterProvidersError$.pipe(take(1)).subscribe(resolve)
+            actions$.next(ParameterProviderActions.configureParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.configureParameterProvider$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.configureParameterProviderSuccess({
+                    response: {
+                        id: mockRequest.id,
+                        parameterProvider: mockResponse,
+                        postUpdateNavigation: mockRequest.postUpdateNavigation,
+                        postUpdateNavigationBoundary: mockRequest.postUpdateNavigationBoundary
+                    }
+                })
+            );
+            expect(mockParameterProviderService.updateParameterProvider).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error with banner when showErrorInContext returns true', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client-id' },
+                    component: { name: 'Updated Parameter Provider' }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const mockError = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+
+            mockParameterProviderService.updateParameterProvider.mockReturnValue(throwError(() => mockError));
+            mockErrorHelper.showErrorInContext.mockReturnValue(true);
+
+            actions$.next(ParameterProviderActions.configureParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.configureParameterProvider$);
+
+            expect(action).toEqual(
+                ParameterProviderActions.parameterProvidersBannerApiError({
+                    error: 'Test error message'
+                })
             );
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(mockError);
+        });
+
+        it('should handle full screen error when showErrorInContext returns false', async () => {
+            const mockRequest = {
+                id: 'test-parameter-provider-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client-id' },
+                    component: { name: 'Updated Parameter Provider' }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const mockError = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            const mockFullScreenErrorAction = { type: 'FULL_SCREEN_ERROR' };
+
+            mockParameterProviderService.updateParameterProvider.mockReturnValue(throwError(() => mockError));
+            mockErrorHelper.showErrorInContext.mockReturnValue(false);
+            mockErrorHelper.fullScreenError.mockReturnValue(mockFullScreenErrorAction);
+
+            actions$.next(ParameterProviderActions.configureParameterProvider({ request: mockRequest }));
+
+            const action = await firstValueFrom(effects.configureParameterProvider$);
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(true, error);
-            expect(result).toEqual(errorAction);
+            expect(action).toEqual(mockFullScreenErrorAction);
+            expect(mockErrorHelper.fullScreenError).toHaveBeenCalledWith(mockError);
         });
     });
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts
index cfb2a545d1..86cae3a77a 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts
@@ -796,4 +796,37 @@ export class ParameterProvidersEffects {
             ),
         { dispatch: false }
     );
+
+    clearParameterProviderBulletins$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ParameterProviderActions.clearParameterProviderBulletins),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.parameterProviderService.clearBulletins({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    })
+                ).pipe(
+                    map((response) =>
+                        ParameterProviderActions.clearParameterProviderBulletinsSuccess({
+                            response: {
+                                componentId: request.componentId,
+                                bulletinsCleared: response.bulletinsCleared || 0,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            ErrorActions.snackBarError({
+                                error: this.errorHelper.getErrorString(errorResponse)
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.spec.ts
new file mode 100644
index 0000000000..bb1e2c9cc7
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.spec.ts
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { parameterProvidersReducer, initialParameterProvidersState } from './parameter-providers.reducer';
+import { clearParameterProviderBulletinsSuccess } from './parameter-providers.actions';
+import { ParameterProvidersState } from './index';
+import { BulletinEntity, ComponentType } from '@nifi/shared';
+
+describe('Parameter Providers Reducer', () => {
+    describe('clearParameterProviderBulletinsSuccess', () => {
+        let testState: ParameterProvidersState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'source1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'source1',
+                    sourceName: 'Test Source',
+                    sourceType: 'COMPONENT',
+                    level: 'INFO',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialParameterProvidersState,
+                parameterProviders: [
+                    {
+                        id: 'parameter-provider-1',
+                        uri: 'test-uri-1',
+                        bulletins: [
+                            createTestBulletin(1, olderTimestamp), // Should be removed
+                            createTestBulletin(2, testTimestamp), // Should be kept (equal)
+                            createTestBulletin(3, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'parameter-provider-1',
+                            name: 'Test Parameter Provider',
+                            type: 'org.apache.nifi.TestParameterProvider',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            affectedComponents: [],
+                            parameterGroupConfigurations: [],
+                            comments: '',
+                            deprecated: false,
+                            extensionMissing: false,
+                            multipleVersionsAvailable: false,
+                            persistsState: false,
+                            referencingParameterContexts: [],
+                            restricted: false,
+                            validationStatus: 'VALID'
+                        }
+                    },
+                    {
+                        id: 'parameter-provider-2',
+                        uri: 'test-uri-2',
+                        bulletins: [
+                            createTestBulletin(4, olderTimestamp) // Should be removed
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'parameter-provider-2',
+                            name: 'Another Parameter Provider',
+                            type: 'org.apache.nifi.AnotherParameterProvider',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: [],
+                            affectedComponents: [],
+                            parameterGroupConfigurations: [],
+                            comments: '',
+                            deprecated: false,
+                            extensionMissing: false,
+                            multipleVersionsAvailable: false,
+                            persistsState: false,
+                            referencingParameterContexts: [],
+                            restricted: false,
+                            validationStatus: 'VALID'
+                        }
+                    }
+                ]
+            };
+        });
+
+        it('should replace bulletins for the specified parameter provider', () => {
+            // Server returns bulletins that remain after clearing (equal and newer bulletins)
+            const action = clearParameterProviderBulletinsSuccess({
+                response: {
+                    componentId: 'parameter-provider-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.ParameterProvider
+                }
+            });
+
+            const newState = parameterProvidersReducer(testState, action);
+
+            const parameterProvider = newState.parameterProviders.find((pp) => pp.id === 'parameter-provider-1');
+            expect(parameterProvider).toBeDefined();
+            expect(parameterProvider!.bulletins).toHaveLength(2);
+            expect(parameterProvider!.bulletins!.map((b) => b.id)).toEqual([2, 3]); // Equal and newer bulletins remain
+        });
+
+        it('should not affect other parameter providers', () => {
+            const action = clearParameterProviderBulletinsSuccess({
+                response: {
+                    componentId: 'parameter-provider-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.ParameterProvider
+                }
+            });
+
+            const newState = parameterProvidersReducer(testState, action);
+
+            const otherParameterProvider = newState.parameterProviders.find((pp) => pp.id === 'parameter-provider-2');
+            expect(otherParameterProvider).toBeDefined();
+            expect(otherParameterProvider!.bulletins).toHaveLength(1); // Original count unchanged
+            expect(otherParameterProvider!.bulletins![0].id).toBe(4);
+        });
+
+        it('should handle parameter provider with no bulletins', () => {
+            testState.parameterProviders[0].bulletins = [] as any;
+
+            const action = clearParameterProviderBulletinsSuccess({
+                response: {
+                    componentId: 'parameter-provider-1',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ParameterProvider
+                }
+            });
+
+            const newState = parameterProvidersReducer(testState, action);
+
+            const parameterProvider = newState.parameterProviders.find((pp) => pp.id === 'parameter-provider-1');
+            expect(parameterProvider).toBeDefined();
+            expect(parameterProvider!.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent parameter provider gracefully', () => {
+            const action = clearParameterProviderBulletinsSuccess({
+                response: {
+                    componentId: 'non-existent-provider',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.ParameterProvider
+                }
+            });
+
+            const newState = parameterProvidersReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.ts
index b705a8ff28..33cc7f6f04 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.reducer.ts
@@ -18,6 +18,7 @@
 import { ParameterProvidersState } from './index';
 import { createReducer, on } from '@ngrx/store';
 import {
+    clearParameterProviderBulletinsSuccess,
     configureParameterProvider,
     configureParameterProviderSuccess,
     createParameterProvider,
@@ -146,5 +147,17 @@ export const parameterProvidersReducer = createReducer(
     on(deleteParameterProviderParametersUpdateRequest, (state) => ({
         ...state,
         saving: false
-    }))
+    })),
+    on(clearParameterProviderBulletinsSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.parameterProviders.findIndex(
+                (provider: any) => provider.id === response.componentId
+            );
+            if (componentIndex > -1) {
+                const provider = draftState.parameterProviders[componentIndex];
+                // Replace bulletins with the current bulletins from the server
+                provider.bulletins = response.bulletins || [];
+            }
+        });
+    })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.actions.ts
index c6402ca8b4..54885e0754 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.actions.ts
@@ -27,6 +27,7 @@ import {
     LoadRegistryClientsResponse,
     SelectRegistryClientRequest
 } from './index';
+import { ClearBulletinsRequest, ClearBulletinsResponse } from '../../../../state/shared';
 
 export const resetRegistryClientsState = createAction('[Registry Clients] Reset Registry Clients State');
 
@@ -103,3 +104,13 @@ export const selectClient = createAction(
     '[Registry Clients] Select Registry Client',
     props<{ request: SelectRegistryClientRequest }>()
 );
+
+export const clearRegistryClientBulletins = createAction(
+    '[Registry Clients] Clear Registry Client Bulletins',
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearRegistryClientBulletinsSuccess = createAction(
+    '[Registry Clients] Clear Registry Client Bulletins Success',
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.spec.ts
index d7064a4149..3c6e05de1b 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,211 +17,507 @@
 
 import { TestBed } from '@angular/core/testing';
 import { provideMockActions } from '@ngrx/effects/testing';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockStore } from '@ngrx/store/testing';
 import { Action } from '@ngrx/store';
-import { ReplaySubject, of, take, throwError } from 'rxjs';
+import { ReplaySubject, of, throwError, firstValueFrom } from 'rxjs';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
 import { HttpErrorResponse } from '@angular/common/http';
 
-import * as RegistryClientsActions from './registry-clients.actions';
 import { RegistryClientsEffects } from './registry-clients.effects';
+import * as RegistryClientsActions from './registry-clients.actions';
 import { RegistryClientService } from '../../service/registry-client.service';
 import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
-import { Client } from '../../../../service/client.service';
-import { NiFiCommon } from '@nifi/shared';
-import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
-import { ExtensionTypesService } from '../../../../service/extension-types.service';
+import { ComponentType } from '@nifi/shared';
+import { RegistryClientEntity } from '../../../../state/shared';
+import { registryClientsFeatureKey } from './index';
 import { initialState } from './registry-clients.reducer';
+import { settingsFeatureKey } from '../index';
+import * as ErrorActions from '../../../../state/error/error.actions';
 
 describe('RegistryClientsEffects', () => {
-    interface SetupOptions {
-        registryClientsState?: any;
-    }
+    let actions$: ReplaySubject<Action>;
+    let effects: RegistryClientsEffects;
+    let mockRegistryClientService: jest.Mocked<RegistryClientService>;
+    let mockErrorHelper: jest.Mocked<ErrorHelper>;
+    let mockDialog: jest.Mocked<MatDialog>;
+
+    beforeEach(() => {
+        const registryClientServiceSpy = {
+            getRegistryClients: jest.fn(),
+            createRegistryClient: jest.fn(),
+            updateRegistryClient: jest.fn(),
+            deleteRegistryClient: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<RegistryClientService>;
+
+        const errorHelperSpy = {
+            handleLoadingError: jest.fn().mockReturnValue({ type: '[Error] Loading Error' }),
+            getErrorString: jest.fn().mockReturnValue('Test error message'),
+            showErrorInContext: jest.fn(),
+            fullScreenError: jest.fn().mockReturnValue({ type: 'FULL_SCREEN_ERROR' })
+        } as unknown as jest.Mocked<ErrorHelper>;
+
+        const dialogSpy = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        } as unknown as jest.Mocked<MatDialog>;
 
-    const mockRegistryClientsResponse = {
-        registries: [],
-        currentTime: '2023-01-01 12:00:00 EST'
-    };
+        const routerSpy = {
+            navigate: jest.fn()
+        } as unknown as jest.Mocked<Router>;
 
-    async function setup({ registryClientsState = initialState }: SetupOptions = {}) {
-        await TestBed.configureTestingModule({
+        TestBed.configureTestingModule({
             providers: [
                 RegistryClientsEffects,
-                provideMockActions(() => action$),
+                provideMockActions(() => actions$),
                 provideMockStore({
                     initialState: {
-                        settings: {
-                            registryClients: registryClientsState
+                        [settingsFeatureKey]: {
+                            [registryClientsFeatureKey]: initialState
                         }
                     }
                 }),
-                {
-                    provide: RegistryClientService,
-                    useValue: {
-                        getRegistryClients: jest.fn()
-                    }
-                },
-                {
-                    provide: ManagementControllerServiceService,
-                    useValue: {}
-                },
-                {
-                    provide: ErrorHelper,
-                    useValue: {
-                        handleLoadingError: jest.fn()
-                    }
-                },
-                {
-                    provide: MatDialog,
-                    useValue: { open: jest.fn(), closeAll: jest.fn() }
-                },
-                {
-                    provide: Router,
-                    useValue: { navigate: jest.fn() }
-                },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: Client, useValue: {} },
-                { provide: NiFiCommon, useValue: {} },
-                { provide: ClusterConnectionService, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} }
+                { provide: RegistryClientService, useValue: registryClientServiceSpy },
+                { provide: ManagementControllerServiceService, useValue: {} },
+                { provide: ErrorHelper, useValue: errorHelperSpy },
+                { provide: Router, useValue: routerSpy },
+                { provide: MatDialog, useValue: dialogSpy },
+                { provide: PropertyTableHelperService, useValue: {} }
             ]
-        }).compileComponents();
-
-        const store = TestBed.inject(MockStore);
-        const effects = TestBed.inject(RegistryClientsEffects);
-        const registryClientService = TestBed.inject(RegistryClientService);
-        const errorHelper = TestBed.inject(ErrorHelper);
-
-        return { effects, store, registryClientService, errorHelper };
-    }
+        });
 
-    let action$: ReplaySubject<Action>;
-    beforeEach(() => {
-        action$ = new ReplaySubject<Action>();
+        effects = TestBed.inject(RegistryClientsEffects);
+        mockRegistryClientService = TestBed.inject(RegistryClientService) as jest.Mocked<RegistryClientService>;
+        mockErrorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        mockDialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        actions$ = new ReplaySubject(1);
     });
 
-    it('should create', async () => {
-        const { effects } = await setup();
-        expect(effects).toBeTruthy();
-    });
+    describe('loadRegistryClients$', () => {
+        it('should dispatch loadRegistryClientsSuccess action when registry clients are loaded successfully', async () => {
+            const mockResponse = {
+                registries: [createMockRegistryClientEntity()],
+                currentTime: '2023-01-01 12:00:00 EST'
+            };
+            mockRegistryClientService.getRegistryClients.mockReturnValue(of(mockResponse));
 
-    describe('Load Registry Clients', () => {
-        it('should load registry clients successfully', async () => {
-            const { effects, registryClientService } = await setup();
+            actions$.next(RegistryClientsActions.loadRegistryClients());
 
-            action$.next(RegistryClientsActions.loadRegistryClients());
-            jest.spyOn(registryClientService, 'getRegistryClients').mockReturnValueOnce(
-                of(mockRegistryClientsResponse) as never
-            );
-            const result = await new Promise((resolve) =>
-                effects.loadRegistryClients$.pipe(take(1)).subscribe(resolve)
-            );
+            const result = await firstValueFrom(effects.loadRegistryClients$);
 
             expect(result).toEqual(
                 RegistryClientsActions.loadRegistryClientsSuccess({
                     response: {
-                        registryClients: mockRegistryClientsResponse.registries,
-                        loadedTimestamp: mockRegistryClientsResponse.currentTime
+                        registryClients: mockResponse.registries,
+                        loadedTimestamp: mockResponse.currentTime
                     }
                 })
             );
+            expect(mockRegistryClientService.getRegistryClients).toHaveBeenCalled();
         });
 
-        it('should fail to load registry clients on initial load with hasExistingData=false', async () => {
-            const { effects, registryClientService } = await setup();
+        it('should dispatch loadRegistryClientsError with status pending on initial load failure', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockRegistryClientService.getRegistryClients.mockReturnValue(throwError(() => errorResponse));
 
-            action$.next(RegistryClientsActions.loadRegistryClients());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(registryClientService, 'getRegistryClients').mockImplementationOnce(() =>
-                throwError(() => error)
-            );
+            actions$.next(RegistryClientsActions.loadRegistryClients());
 
-            const result = await new Promise((resolve) =>
-                effects.loadRegistryClients$.pipe(take(1)).subscribe(resolve)
-            );
+            const result = await firstValueFrom(effects.loadRegistryClients$);
 
             expect(result).toEqual(
                 RegistryClientsActions.loadRegistryClientsError({
-                    errorResponse: error,
+                    errorResponse,
                     loadedTimestamp: initialState.loadedTimestamp,
                     status: 'pending'
                 })
             );
         });
+    });
+
+    describe('loadRegistryClientsError$', () => {
+        it('should handle error on initial load (hasExistingData=false)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = RegistryClientsActions.registryClientsBannerApiError({ error: 'Error loading' });
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
+                RegistryClientsActions.loadRegistryClientsError({
+                    errorResponse,
+                    loadedTimestamp: initialState.loadedTimestamp,
+                    status: 'pending'
+                })
+            );
+
+            const result = await firstValueFrom(effects.loadRegistryClientsError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(false, errorResponse);
+            expect(result).toEqual(errorAction);
+        });
 
-        it('should fail to load registry clients on refresh with hasExistingData=true', async () => {
-            const stateWithData = {
-                ...initialState,
-                loadedTimestamp: '2023-01-01 11:00:00 EST'
+        it('should handle error on refresh (hasExistingData=true)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = RegistryClientsActions.registryClientsBannerApiError({ error: 'Error loading' });
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
+                RegistryClientsActions.loadRegistryClientsError({
+                    errorResponse,
+                    loadedTimestamp: '2023-01-01 11:00:00 EST',
+                    status: 'success'
+                })
+            );
+
+            const result = await firstValueFrom(effects.loadRegistryClientsError$);
+
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(true, errorResponse);
+            expect(result).toEqual(errorAction);
+        });
+    });
+
+    describe('createRegistryClient$', () => {
+        it('should dispatch createRegistryClientSuccess action when registry client is created successfully', async () => {
+            const mockRequest = {
+                revision: { version: 0, clientId: 'test-client' },
+                disconnectedNodeAcknowledged: false,
+                component: {
+                    name: 'Test Registry Client',
+                    type: 'org.apache.nifi.TestRegistryClient',
+                    bundle: {
+                        group: 'org.apache.nifi',
+                        artifact: 'test-bundle',
+                        version: '1.0.0'
+                    },
+                    description: 'Test Description'
+                }
             };
-            const { effects, registryClientService } = await setup({
-                registryClientsState: stateWithData
-            });
+            const mockResponse = createMockRegistryClientEntity();
+            mockRegistryClientService.createRegistryClient.mockReturnValue(of(mockResponse));
+
+            actions$.next(RegistryClientsActions.createRegistryClient({ request: mockRequest }));
 
-            action$.next(RegistryClientsActions.loadRegistryClients());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(registryClientService, 'getRegistryClients').mockImplementationOnce(() =>
-                throwError(() => error)
+            const result = await firstValueFrom(effects.createRegistryClient$);
+
+            expect(result).toEqual(
+                RegistryClientsActions.createRegistryClientSuccess({
+                    response: {
+                        registryClient: mockResponse
+                    }
+                })
             );
+            expect(mockRegistryClientService.createRegistryClient).toHaveBeenCalledWith(mockRequest);
+        });
+
+        it('should handle error and close dialog when creating registry client fails', async () => {
+            const mockRequest = {
+                revision: { version: 0, clientId: 'test-client' },
+                disconnectedNodeAcknowledged: false,
+                component: {
+                    name: 'Test Registry Client',
+                    type: 'org.apache.nifi.TestRegistryClient',
+                    bundle: {
+                        group: 'org.apache.nifi',
+                        artifact: 'test-bundle',
+                        version: '1.0.0'
+                    },
+                    description: 'Test Description'
+                }
+            };
+            const errorResponse = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            mockRegistryClientService.createRegistryClient.mockReturnValue(throwError(() => errorResponse));
 
-            const result = await new Promise((resolve) =>
-                effects.loadRegistryClients$.pipe(take(1)).subscribe(resolve)
+            actions$.next(RegistryClientsActions.createRegistryClient({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.createRegistryClient$);
+
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                RegistryClientsActions.registryClientsSnackbarApiError({
+                    error: 'Test error message'
+                })
             );
+        });
+    });
+
+    describe('deleteRegistryClient$', () => {
+        it('should dispatch deleteRegistryClientSuccess action when registry client is deleted successfully', async () => {
+            const mockRequest = {
+                registryClient: createMockRegistryClientEntity()
+            };
+            const mockResponse = createMockRegistryClientEntity();
+            mockRegistryClientService.deleteRegistryClient.mockReturnValue(of(mockResponse));
+
+            actions$.next(RegistryClientsActions.deleteRegistryClient({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.deleteRegistryClient$);
 
             expect(result).toEqual(
-                RegistryClientsActions.loadRegistryClientsError({
-                    errorResponse: error,
-                    loadedTimestamp: stateWithData.loadedTimestamp,
-                    status: 'success'
+                RegistryClientsActions.deleteRegistryClientSuccess({
+                    response: {
+                        registryClient: mockResponse
+                    }
                 })
             );
+            expect(mockRegistryClientService.deleteRegistryClient).toHaveBeenCalledWith(mockRequest);
         });
 
-        it('should handle loadRegistryClientsError with pending status (no existing data)', async () => {
-            const { effects, errorHelper } = await setup();
+        it('should handle error when deleting registry client fails', async () => {
+            const mockRequest = {
+                registryClient: createMockRegistryClientEntity()
+            };
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockRegistryClientService.deleteRegistryClient.mockReturnValue(throwError(() => errorResponse));
 
-            action$.next(
-                RegistryClientsActions.loadRegistryClientsError({
-                    errorResponse: new HttpErrorResponse({ status: 500 }),
-                    loadedTimestamp: initialState.loadedTimestamp,
-                    status: 'pending'
+            actions$.next(RegistryClientsActions.deleteRegistryClient({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.deleteRegistryClient$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                RegistryClientsActions.registryClientsSnackbarApiError({
+                    error: 'Test error message'
                 })
             );
+        });
+    });
 
-            const errorAction = RegistryClientsActions.registryClientsBannerApiError({ error: 'Error loading' });
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+    describe('clearRegistryClientBulletins$', () => {
+        it('should dispatch clearRegistryClientBulletinsSuccess action when bulletins are cleared successfully', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowRegistryClient
+            };
+            const mockResponse = {
+                bulletinsCleared: 5,
+                bulletins: []
+            };
+            mockRegistryClientService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(RegistryClientsActions.clearRegistryClientBulletins({ request: mockRequest }));
 
-            const result = await new Promise((resolve) =>
-                effects.loadRegistryClientsError$.pipe(take(1)).subscribe(resolve)
+            const result = await firstValueFrom(effects.clearRegistryClientBulletins$);
+
+            expect(result).toEqual(
+                RegistryClientsActions.clearRegistryClientBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: 5,
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
+                })
             );
+            expect(mockRegistryClientService.clearBulletins).toHaveBeenCalledWith({
+                uri: mockRequest.uri,
+                fromTimestamp: mockRequest.fromTimestamp
+            });
+        });
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(false, expect.any(HttpErrorResponse));
-            expect(result).toEqual(errorAction);
+        it('should handle undefined bulletinsCleared response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowRegistryClient
+            };
+            const mockResponse = {
+                bulletins: []
+            }; // No bulletinsCleared property
+            mockRegistryClientService.clearBulletins.mockReturnValue(of(mockResponse));
+
+            actions$.next(RegistryClientsActions.clearRegistryClientBulletins({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.clearRegistryClientBulletins$);
+
+            expect(result).toEqual(
+                RegistryClientsActions.clearRegistryClientBulletinsSuccess({
+                    response: {
+                        componentId: mockRequest.componentId,
+                        bulletinsCleared: 0, // Should default to 0
+                        bulletins: mockResponse.bulletins || [],
+                        componentType: mockRequest.componentType
+                    }
+                })
+            );
         });
 
-        it('should handle loadRegistryClientsError with success status (existing data)', async () => {
-            const { effects, errorHelper } = await setup();
+        it('should pass through all required fields in the success response', async () => {
+            const mockRequest = {
+                uri: 'test-uri',
+                fromTimestamp: '2023-01-01T12:00:00.000Z',
+                componentId: 'test-component-id',
+                componentType: ComponentType.FlowRegistryClient
+            };
+            const mockResponse = {
+                bulletinsCleared: 3,
+                bulletins: []
+            };
+            mockRegistryClientService.clearBulletins.mockReturnValue(of(mockResponse));
 
-            action$.next(
-                RegistryClientsActions.loadRegistryClientsError({
-                    errorResponse: new HttpErrorResponse({ status: 503 }),
-                    loadedTimestamp: '2023-01-01 11:00:00 EST',
-                    status: 'success'
+            actions$.next(RegistryClientsActions.clearRegistryClientBulletins({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.clearRegistryClientBulletins$);
+
+            const successAction = result as any;
+            expect(successAction.response.componentId).toBe(mockRequest.componentId);
+            expect(successAction.response.componentType).toBe(mockRequest.componentType);
+            expect(successAction.response.bulletinsCleared).toBe(3);
+        });
+    });
+
+    describe('configureRegistryClient$', () => {
+        it('should dispatch configureRegistryClientSuccess action when registry client is configured successfully', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client' },
+                    component: {
+                        id: 'test-id',
+                        name: 'Updated Registry Client'
+                    }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const mockResponse = createMockRegistryClientEntity();
+            mockRegistryClientService.updateRegistryClient.mockReturnValue(of(mockResponse));
+
+            actions$.next(RegistryClientsActions.configureRegistryClient({ request: mockRequest }));
+
+            const result = await firstValueFrom(effects.configureRegistryClient$);
+
+            expect(result).toEqual(
+                RegistryClientsActions.configureRegistryClientSuccess({
+                    response: {
+                        id: mockRequest.id,
+                        registryClient: mockResponse,
+                        postUpdateNavigation: mockRequest.postUpdateNavigation,
+                        postUpdateNavigationBoundary: mockRequest.postUpdateNavigationBoundary
+                    }
                 })
             );
+            expect(mockRegistryClientService.updateRegistryClient).toHaveBeenCalledWith(mockRequest);
+        });
 
-            const errorAction = RegistryClientsActions.registryClientsBannerApiError({ error: 'Error loading' });
-            jest.spyOn(errorHelper, 'handleLoadingError').mockReturnValueOnce(errorAction);
+        it('should handle error with banner when showErrorInContext returns true', async () => {
+            const mockRequest = {
+                id: 'test-id',
+                uri: 'test-uri',
+                payload: {
+                    revision: { version: 1, clientId: 'test-client' },
+                    component: {
+                        id: 'test-id',
+                        name: 'Updated Registry Client'
+                    }
+                },
+                postUpdateNavigation: undefined,
+                postUpdateNavigationBoundary: undefined
+            };
+            const errorResponse = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            mockRegistryClientService.updateRegistryClient.mockReturnValue(throwError(() => errorResponse));
+
+            actions$.next(RegistryClientsActions.configureRegistryClient({ request: mockRequest }));
 
-            const result = await new Promise((resolve) =>
-                effects.loadRegistryClientsError$.pipe(take(1)).subscribe(resolve)
+            const result = await firstValueFrom(effects.configureRegistryClient$);
+
+            expect(mockErrorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(result).toEqual(
+                RegistryClientsActions.registryClientsBannerApiError({
+                    error: 'Test error message'
+                })
             );
+        });
+    });
 
-            expect(errorHelper.handleLoadingError).toHaveBeenCalledWith(true, expect.any(HttpErrorResponse));
-            expect(result).toEqual(errorAction);
+    describe('createRegistryClientSuccess$', () => {
+        it('should close dialog and dispatch selectClient action', async () => {
+            const mockResponse = {
+                registryClient: createMockRegistryClientEntity()
+            };
+
+            actions$.next(RegistryClientsActions.createRegistryClientSuccess({ response: mockResponse }));
+
+            const result = await firstValueFrom(effects.createRegistryClientSuccess$);
+
+            expect(mockDialog.closeAll).toHaveBeenCalled();
+            expect(result).toEqual(
+                RegistryClientsActions.selectClient({
+                    request: {
+                        id: mockResponse.registryClient.id
+                    }
+                })
+            );
         });
     });
+
+    describe('registryClientsBannerApiError$', () => {
+        it('should dispatch addBannerError action', async () => {
+            const errorMessage = 'Test error message';
+
+            actions$.next(RegistryClientsActions.registryClientsBannerApiError({ error: errorMessage }));
+
+            const result = await firstValueFrom(effects.registryClientsBannerApiError$);
+
+            expect(result).toEqual(
+                ErrorActions.addBannerError({
+                    errorContext: {
+                        errors: [errorMessage],
+                        context: 'registry-clients' as any
+                    }
+                })
+            );
+        });
+    });
+
+    describe('registryClientsSnackbarApiError$', () => {
+        it('should dispatch snackBarError action', async () => {
+            const errorMessage = 'Test error message';
+
+            actions$.next(RegistryClientsActions.registryClientsSnackbarApiError({ error: errorMessage }));
+
+            const result = await firstValueFrom(effects.registryClientsSnackbarApiError$);
+
+            expect(result).toEqual(ErrorActions.snackBarError({ error: errorMessage }));
+        });
+    });
+
+    // Mock data factory
+    function createMockRegistryClientEntity(): RegistryClientEntity {
+        return {
+            id: 'test-registry-client-id',
+            uri: 'test-uri',
+            permissions: {
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 1,
+                clientId: 'test-client'
+            },
+            component: {
+                id: 'test-registry-client-id',
+                name: 'Test Registry Client',
+                description: 'Test Description',
+                type: 'org.apache.nifi.TestRegistryClient',
+                bundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-bundle',
+                    version: '1.0.0'
+                },
+                properties: {},
+                descriptors: {},
+                validationErrors: [],
+                validationStatus: 'VALID'
+            },
+            bulletins: []
+        };
+    }
 });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.ts
index 1912319f7c..ec57ecbd9d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.effects.ts
@@ -439,4 +439,37 @@ export class RegistryClientsEffects {
             ),
         { dispatch: false }
     );
+
+    clearRegistryClientBulletins$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(RegistryClientsActions.clearRegistryClientBulletins),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.registryClientService.clearBulletins({
+                        uri: request.uri,
+                        fromTimestamp: request.fromTimestamp
+                    })
+                ).pipe(
+                    map((response) =>
+                        RegistryClientsActions.clearRegistryClientBulletinsSuccess({
+                            response: {
+                                componentId: request.componentId,
+                                bulletinsCleared: response.bulletinsCleared || 0,
+                                bulletins: response.bulletins || [],
+                                componentType: request.componentType
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            RegistryClientsActions.registryClientsSnackbarApiError({
+                                error: this.errorHelper.getErrorString(errorResponse)
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.spec.ts
new file mode 100644
index 0000000000..20a9769c42
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.spec.ts
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { registryClientsReducer, initialState } from './registry-clients.reducer';
+import { clearRegistryClientBulletinsSuccess } from './registry-clients.actions';
+import { RegistryClientsState } from './index';
+import { BulletinEntity, ComponentType } from '@nifi/shared';
+
+describe('Registry Clients Reducer', () => {
+    describe('clearRegistryClientBulletinsSuccess', () => {
+        let testState: RegistryClientsState;
+        const testTimestamp = new Date('2023-01-01T12:00:00.000Z').getTime();
+        const olderTimestamp = new Date('2023-01-01T11:00:00.000Z').getTime();
+        const newerTimestamp = new Date('2023-01-01T13:00:00.000Z').getTime();
+
+        const createTestBulletin = (id: number, timestamp: number): BulletinEntity => {
+            const isoString = new Date(timestamp).toISOString();
+            const timeOnly = isoString.substring(11, 19) + ' UTC'; // Extract HH:MM:SS and add UTC
+            return {
+                id,
+                canRead: true,
+                sourceId: 'source1',
+                groupId: 'group1',
+                timestamp: timeOnly,
+                timestampIso: isoString,
+                bulletin: {
+                    id,
+                    nodeAddress: 'localhost',
+                    category: 'test',
+                    groupId: 'group1',
+                    sourceId: 'source1',
+                    sourceName: 'Test Source',
+                    sourceType: 'COMPONENT',
+                    level: 'DEBUG',
+                    message: `Test message ${id}`,
+                    timestamp: timeOnly,
+                    timestampIso: isoString
+                }
+            };
+        };
+
+        beforeEach(() => {
+            testState = {
+                ...initialState,
+                registryClients: [
+                    {
+                        id: 'registry-client-1',
+                        uri: 'test-uri-1',
+                        bulletins: [
+                            createTestBulletin(1, olderTimestamp), // Should be removed
+                            createTestBulletin(2, testTimestamp), // Should be kept (equal)
+                            createTestBulletin(3, newerTimestamp) // Should be kept
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'registry-client-1',
+                            name: 'Test Registry Client',
+                            type: 'org.apache.nifi.TestRegistryClient',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: []
+                        }
+                    },
+                    {
+                        id: 'registry-client-2',
+                        uri: 'test-uri-2',
+                        bulletins: [
+                            createTestBulletin(4, olderTimestamp) // Should be removed
+                        ],
+                        revision: { version: 1, clientId: 'test' },
+                        permissions: { canRead: true, canWrite: true },
+                        component: {
+                            id: 'registry-client-2',
+                            name: 'Another Registry Client',
+                            type: 'org.apache.nifi.AnotherRegistryClient',
+                            bundle: { group: 'org.apache.nifi', artifact: 'test', version: '1.0.0' },
+                            properties: {},
+                            descriptors: {},
+                            validationErrors: []
+                        }
+                    }
+                ]
+            };
+        });
+
+        it('should replace bulletins for the specified registry client', () => {
+            // Server returns bulletins that remain after clearing (equal and newer bulletins)
+            const action = clearRegistryClientBulletinsSuccess({
+                response: {
+                    componentId: 'registry-client-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.FlowRegistryClient
+                }
+            });
+
+            const newState = registryClientsReducer(testState, action);
+
+            const registryClient = newState.registryClients.find((rc) => rc.id === 'registry-client-1');
+            expect(registryClient).toBeDefined();
+            expect(registryClient!.bulletins).toHaveLength(2);
+            expect(registryClient!.bulletins!.map((b) => b.id)).toEqual([2, 3]); // Equal and newer bulletins remain
+        });
+
+        it('should not affect other registry clients', () => {
+            const action = clearRegistryClientBulletinsSuccess({
+                response: {
+                    componentId: 'registry-client-1',
+                    bulletinsCleared: 1,
+                    bulletins: [createTestBulletin(2, testTimestamp), createTestBulletin(3, newerTimestamp)],
+                    componentType: ComponentType.FlowRegistryClient
+                }
+            });
+
+            const newState = registryClientsReducer(testState, action);
+
+            const otherRegistryClient = newState.registryClients.find((rc) => rc.id === 'registry-client-2');
+            expect(otherRegistryClient).toBeDefined();
+            expect(otherRegistryClient!.bulletins).toHaveLength(1); // Original count unchanged
+            expect(otherRegistryClient!.bulletins![0].id).toBe(4);
+        });
+
+        it('should handle registry client with no bulletins', () => {
+            testState.registryClients[0].bulletins = undefined;
+
+            const action = clearRegistryClientBulletinsSuccess({
+                response: {
+                    componentId: 'registry-client-1',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.FlowRegistryClient
+                }
+            });
+
+            const newState = registryClientsReducer(testState, action);
+
+            const registryClient = newState.registryClients.find((rc) => rc.id === 'registry-client-1');
+            expect(registryClient).toBeDefined();
+            expect(registryClient!.bulletins).toEqual([]);
+        });
+
+        it('should handle non-existent registry client gracefully', () => {
+            const action = clearRegistryClientBulletinsSuccess({
+                response: {
+                    componentId: 'non-existent-client',
+                    bulletinsCleared: 0,
+                    bulletins: [],
+                    componentType: ComponentType.FlowRegistryClient
+                }
+            });
+
+            const newState = registryClientsReducer(testState, action);
+
+            // State should remain unchanged
+            expect(newState).toEqual(testState);
+        });
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.ts
index a9f12a0178..453836d59e 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/registry-clients/registry-clients.reducer.ts
@@ -19,6 +19,7 @@ import { createReducer, on } from '@ngrx/store';
 import { produce } from 'immer';
 import { RegistryClientsState } from './index';
 import {
+    clearRegistryClientBulletinsSuccess,
     configureRegistryClient,
     configureRegistryClientSuccess,
     createRegistryClient,
@@ -92,5 +93,17 @@ export const registryClientsReducer = createReducer(
             }
             draftState.saving = false;
         });
+    }),
+    on(clearRegistryClientBulletinsSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.registryClients.findIndex(
+                (client: any) => client.id === response.componentId
+            );
+            if (componentIndex > -1) {
+                const client = draftState.registryClients[componentIndex];
+                // Replace bulletins with the current bulletins from the server
+                client.bulletins = response.bulletins || [];
+            }
+        });
     })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.actions.ts
index 9cb54e2776..9fefe5809c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.actions.ts
@@ -31,7 +31,7 @@ import {
     StopReportingTaskRequest,
     StopReportingTaskSuccess
 } from './index';
-import { FetchComponentVersionsRequest } from '../../../../state/shared';
+import { ClearBulletinsRequest, ClearBulletinsResponse, FetchComponentVersionsRequest } from '../../../../state/shared';
 
 export const resetReportingTasksState = createAction('[Reporting Tasks] Reset Reporting Tasks State');
 
@@ -143,3 +143,13 @@ export const openChangeReportingTaskVersionDialog = createAction(
     `[Reporting Tasks] Open Change Reporting Task Version Dialog`,
     props<{ request: FetchComponentVersionsRequest }>()
 );
+
+export const clearReportingTaskBulletins = createAction(
+    '[Reporting Tasks] Clear Reporting Task Bulletins',
+    props<{ request: ClearBulletinsRequest }>()
+);
+
+export const clearReportingTaskBulletinsSuccess = createAction(
+    '[Reporting Tasks] Clear Reporting Task Bulletins Success',
+    props<{ response: ClearBulletinsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.spec.ts
index 391b44cec8..047bd6b51a 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.spec.ts
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *    http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,175 +17,619 @@
 
 import { TestBed } from '@angular/core/testing';
 import { provideMockActions } from '@ngrx/effects/testing';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { provideMockStore } from '@ngrx/store/testing';
 import { Action } from '@ngrx/store';
-import { ReplaySubject, of, take, throwError } from 'rxjs';
+import { ReplaySubject, of, throwError, firstValueFrom } from 'rxjs';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
 import { HttpErrorResponse } from '@angular/common/http';
 
-import * as ReportingTaskActions from './reporting-tasks.actions';
 import { ReportingTasksEffects } from './reporting-tasks.effects';
+import * as ReportingTasksActions from './reporting-tasks.actions';
 import { ReportingTaskService } from '../../service/reporting-task.service';
 import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
 import { ErrorHelper } from '../../../../service/error-helper.service';
-import { MatDialog } from '@angular/material/dialog';
-import { Router } from '@angular/router';
 import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
 import { ExtensionTypesService } from '../../../../service/extension-types.service';
+import { ComponentType } from '@nifi/shared';
+import { ReportingTaskEntity, reportingTasksFeatureKey } from './index';
 import { initialState } from './reporting-tasks.reducer';
+import { settingsFeatureKey } from '../index';
+import * as ErrorActions from '../../../../state/error/error.actions';
 
 describe('ReportingTasksEffects', () => {
-    interface SetupOptions {
-        reportingTasksState?: any;
-    }
+    let actions$: ReplaySubject<Action>;
+    let effects: ReportingTasksEffects;
+    let mockReportingTaskService: jest.Mocked<ReportingTaskService>;
+    let mockErrorHelper: jest.Mocked<ErrorHelper>;
+    let mockDialog: jest.Mocked<MatDialog>;
+
+    beforeEach(() => {
+        const reportingTaskServiceSpy = {
+            getReportingTasks: jest.fn(),
+            createReportingTask: jest.fn(),
+            updateReportingTask: jest.fn(),
+            deleteReportingTask: jest.fn(),
+            startReportingTask: jest.fn(),
+            stopReportingTask: jest.fn(),
+            clearBulletins: jest.fn()
+        } as unknown as jest.Mocked<ReportingTaskService>;
+
+        const errorHelperSpy = {
+            handleLoadingError: jest.fn().mockReturnValue({ type: '[Error] Loading Error' }),
+            getErrorString: jest.fn().mockReturnValue('Test error message'),
+            showErrorInContext: jest.fn(),
+            fullScreenError: jest.fn().mockReturnValue({ type: 'FULL_SCREEN_ERROR' })
+        } as unknown as jest.Mocked<ErrorHelper>;
+
+        const dialogSpy = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        } as unknown as jest.Mocked<MatDialog>;
 
-    const mockResponse = {
-        reportingTasks: [],
-        currentTime: '2023-01-01 12:00:00 EST'
-    };
+        const routerSpy = {
+            navigate: jest.fn()
+        } as unknown as jest.Mocked<Router>;
 
-    async function setup({ reportingTasksState = initialState }: SetupOptions = {}) {
-        await TestBed.configureTestingModule({
+        const propertyTableHelperServiceSpy = {
+            getComponentHistory: jest.fn(),
+            createNewProperty: jest.fn(),
+            createNewService: jest.fn()
+        } as unknown as jest.Mocked<PropertyTableHelperService>;
+
+        const extensionTypesServiceSpy = {
+            getReportingTaskVersionsForType: jest.fn()
+        } as unknown as jest.Mocked<ExtensionTypesService>;
+
+        TestBed.configureTestingModule({
             providers: [
                 ReportingTasksEffects,
-                provideMockActions(() => action$),
+                provideMockActions(() => actions$),
                 provideMockStore({
                     initialState: {
-                        settings: {
-                            reportingTasks: reportingTasksState
+                        [settingsFeatureKey]: {
+                            [reportingTasksFeatureKey]: initialState
                         }
                     }
                 }),
-                { provide: ReportingTaskService, useValue: { getReportingTasks: jest.fn() } },
+                { provide: ReportingTaskService, useValue: reportingTaskServiceSpy },
                 { provide: ManagementControllerServiceService, useValue: {} },
-                { provide: ErrorHelper, useValue: { handleLoadingError: jest.fn() } },
-                { provide: MatDialog, useValue: { open: jest.fn(), closeAll: jest.fn() } },
-                { provide: Router, useValue: { navigate: jest.fn() } },
-                { provide: PropertyTableHelperService, useValue: {} },
-                { provide: ExtensionTypesService, useValue: {} }
+                { provide: ErrorHelper, useValue: errorHelperSpy },
+                { provide: Router, useValue: routerSpy },
+                { provide: MatDialog, useValue: dialogSpy },
+                { provide: PropertyTableHelperService, useValue: propertyTableHelperServiceSpy },
+                { provide: ExtensionTypesService, useValue: extensionTypesServiceSpy }
             ]
-        }).compileComponents();
-
-        const store = TestBed.inject(MockStore);
-        const effects = TestBed.inject(ReportingTasksEffects);
-        const reportingTaskService = TestBed.inject(ReportingTaskService);
-        const errorHelper = TestBed.inject(ErrorHelper);
-
-        return { effects, store, reportingTaskService, errorHelper };
-    }
-
-    let action$: ReplaySubject<Action>;
-    beforeEach(() => {
-        action$ = new ReplaySubject<Action>();
-    });
+        });
 
-    it('should create', async () => {
-        const { effects } = await setup();
-        expect(effects).toBeTruthy();
+        effects = TestBed.inject(ReportingTasksEffects);
+        mockReportingTaskService = TestBed.inject(ReportingTaskService) as jest.Mocked<ReportingTaskService>;
+        mockErrorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        mockDialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        actions$ = new ReplaySubject(1);
     });
 
-    describe('Load Reporting Tasks', () => {
-        it('should load reporting tasks successfully', async () => {
-            const { effects, reportingTaskService } = await setup();
+    describe('loadReportingTasks$', () => {
+        it('should dispatch loadReportingTasksSuccess action when reporting tasks are loaded successfully', async () => {
+            const mockResponse = {
+                reportingTasks: [createMockReportingTaskEntity()],
+                currentTime: '2023-01-01 12:00:00 EST'
+            };
+            mockReportingTaskService.getReportingTasks.mockReturnValue(of(mockResponse));
 
-            action$.next(ReportingTaskActions.loadReportingTasks());
-            jest.spyOn(reportingTaskService, 'getReportingTasks').mockReturnValueOnce(of(mockResponse) as never);
+            actions$.next(ReportingTasksActions.loadReportingTasks());
 
-            const result = await new Promise((resolve) => effects.loadReportingTasks$.pipe(take(1)).subscribe(resolve));
+            const result = await firstValueFrom(effects.loadReportingTasks$);
 
             expect(result).toEqual(
-                ReportingTaskActions.loadReportingTasksSuccess({
+                ReportingTasksActions.loadReportingTasksSuccess({
                     response: {
                         reportingTasks: mockResponse.reportingTasks,
                         loadedTimestamp: mockResponse.currentTime
                     }
                 })
             );
+            expect(mockReportingTaskService.getReportingTasks).toHaveBeenCalled();
         });
 
-        it('should fail to load reporting tasks on initial load with hasExistingData=false', async () => {
-            const { effects, reportingTaskService } = await setup();
+        it('should dispatch loadReportingTasksError with status pending on initial load failure', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error' });
+            mockReportingTaskService.getReportingTasks.mockReturnValue(throwError(() => errorResponse));
 
-            action$.next(ReportingTaskActions.loadReportingTasks());
-            const error = new HttpErrorResponse({ status: 500 });
-            jest.spyOn(reportingTaskService, 'getReportingTasks').mockImplementationOnce(() => throwError(() => error));
+            actions$.next(ReportingTasksActions.loadReportingTasks());
 
-            const result = await new Promise((resolve) => effects.loadReportingTasks$.pipe(take(1)).subscribe(resolve));
+            const result = await firstValueFrom(effects.loadReportingTasks$);
 
+            // On initial load (no existing timestamp), status should be 'pending'
             expect(result).toEqual(
-                ReportingTaskActions.loadReportingTasksError({
-                    errorResponse: error,
+                ReportingTasksActions.loadReportingTasksError({
+                    errorResponse,
+                    loadedTimestamp: initialState.loadedTimestamp,
+                    status: 'pending'
+                })
+            );
+        });
+    });
+
+    describe('loadReportingTasksError$', () => {
+        it('should handle error on initial load (hasExistingData=false)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = ReportingTasksActions.reportingTasksBannerApiError({ error: 'Error loading' });
+
+            mockErrorHelper.handleLoadingError.mockReturnValue(errorAction);
+
+            actions$.next(
+                ReportingTasksActions.loadReportingTasksError({
+                    errorResponse,
                     loadedTimestamp: initialState.loadedTimestamp,
                     status: 'pending'
                 })
             );
+
+            const result = await firstValueFrom(effects.loadReportingTasksError$);
+
+            // Should call handleLoadingError with false for initial load
+            expect(mockErrorHelper.handleLoadingError).toHaveBeenCalledWith(false, errorResponse);
+            expect(result).toEqual(errorAction);
+        });
+
+        it('should handle error on refresh (hasExistingData=true)', async () => {
+            const errorResponse = new HttpErrorResponse({ status: 500 });
+            const errorAction = ReportingTasksActions.reportingTasksBannerApiError({ error: 'Error loading' });
+
... 5795 lines suppressed ...