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

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 69264b6dd1381875ba6cb0011d78eb197bc44f29
Author: Eric Pugh <[email protected]>
AuthorDate: Tue Oct 27 14:31:14 2026 -0400

    SOLR-16398: Migrate admin UI's replication details fetch to the v2 API 
(#5241)
    
    (cherry picked from commit bbfbe465d88107fd8b7b193bd4532ff092892e39)
---
 .../SOLR-16398-admin-ui-replication-details-v2.yml |  13 +++
 .../AdminUiCoreOverviewReplicationErrorsTest.java  | 121 +++++++++++++++++++++
 .../web/js/angular/controllers/core-overview.js    |  32 ++++--
 .../web/js/angular/controllers/replication.js      |  67 +++++++-----
 solr/webapp/web/js/angular/services.js             |   4 +
 5 files changed, 201 insertions(+), 36 deletions(-)

diff --git 
a/changelog/unreleased/SOLR-16398-admin-ui-replication-details-v2.yml 
b/changelog/unreleased/SOLR-16398-admin-ui-replication-details-v2.yml
new file mode 100644
index 00000000000..62231a45495
--- /dev/null
+++ b/changelog/unreleased/SOLR-16398-admin-ui-replication-details-v2.yml
@@ -0,0 +1,13 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title:
+  Admin UI's Replication screen and core overview page now fetch replication 
status via the v2
+  GET /api/cores/{coreName}/replication API instead of the v1 replication 
handler.
+type: changed
+authors:
+  - name: Eric Pugh
+    nick: epugh
+  - name: Nick Shanin
+    nick: nick-boss-tech
+links:
+  - name: SOLR-16398
+    url: https://issues.apache.org/jira/browse/SOLR-16398
diff --git 
a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreOverviewReplicationErrorsTest.java
 
b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreOverviewReplicationErrorsTest.java
new file mode 100644
index 00000000000..fe2c4a1a0c9
--- /dev/null
+++ 
b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreOverviewReplicationErrorsTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.solr.webapp;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Properties;
+import org.apache.solr.embedded.JettyConfig;
+import org.apache.solr.embedded.JettySolrRunner;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+
+/**
+ * Covers {@code core-overview.js}'s two replication-fetch-failure paths: a 
generic failure shows
+ * the inline "not configured" message, while a 401/403 routes through {@code 
ApiErrorHandler}
+ * instead.
+ */
+public class AdminUiCoreOverviewReplicationErrorsTest extends 
AdminUiStandaloneTestBase {
+
+  private static final String CORE = "replicationerrors";
+
+  /** 0 = pass through; otherwise the status the filter forces for this core's 
/replication call. */
+  private static volatile int forcedReplicationStatus = 0;
+
+  @BeforeClass
+  public static void startStandaloneNode() throws Exception {
+    Path home = buildStandaloneHome(CORE);
+    JettyConfig.Builder config = JettyConfig.builder();
+    config.withFilter(ForceReplicationStatusFilter.class, "/*");
+    configureJettyForUi(config);
+    standaloneJetty = new JettySolrRunner(home.toString(), new Properties(), 
config.build());
+    standaloneJetty.start();
+    baseUrl = standaloneJetty.getBaseUrl().toString();
+  }
+
+  @AfterClass
+  public static void stopStandaloneNode() throws Exception {
+    if (standaloneJetty != null) {
+      standaloneJetty.stop();
+      standaloneJetty = null;
+    }
+  }
+
+  @Test
+  public void testGenericReplicationFailureShowsInlineMessage() {
+    forcedReplicationStatus = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
+    openPage(CORE + "/core-overview", By.id("dashboard"));
+
+    waitForTextContains(By.cssSelector("#replication .message-container"), 
"not configured");
+    assertNoSevereConsoleErrors("/replication");
+  }
+
+  @Test
+  public void testReplicationForbiddenDoesNotShowConfigurationMessage() throws 
Exception {
+    // Not asserting #authz-failures itself: app.js's httpInterceptor clears
+    // $rootScope.showAuthzFailures on every successful v1 call, and 
core-overview fires several
+    // of those (Luke, CoreInfo, Ping) alongside this one, so the banner isn't 
reliably still up
+    // by the time we'd check it. This test only guards the message text below.
+    forcedReplicationStatus = HttpServletResponse.SC_FORBIDDEN;
+    openPage(CORE + "/core-overview", By.id("dashboard"));
+
+    // replicationMessage is always set once the request settles (to the 
message, or to null
+    // for an auth failure), so waiting for it to be defined signals the 
request is done.
+    waitUntil(
+        "the replication-details request should have completed", 
this::replicationRequestSettled);
+    assertTrue(
+        driver.findElements(By.cssSelector("#replication 
.message-container")).stream()
+            .noneMatch(e -> e.getText().contains("not configured")));
+    assertNoSevereConsoleErrors("/replication");
+  }
+
+  private boolean replicationRequestSettled() {
+    Object settled =
+        ((JavascriptExecutor) driver)
+            .executeScript(
+                "var el = document.querySelector('#replication');"
+                    + "if (!el) return false;"
+                    + "var scope = angular.element(el).scope();"
+                    + "return scope.replicationMessage !== undefined;");
+    return Boolean.TRUE.equals(settled);
+  }
+
+  /** Forces the configured HTTP status for this core's v2 replication-details 
call, when armed. */
+  public static class ForceReplicationStatusFilter implements Filter {
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
+        throws IOException, ServletException {
+      HttpServletRequest req = (HttpServletRequest) request;
+      if (forcedReplicationStatus != 0
+          && req.getRequestURI().endsWith("/cores/" + CORE + "/replication")) {
+        ((HttpServletResponse) response).sendError(forcedReplicationStatus);
+        return;
+      }
+      chain.doFilter(request, response);
+    }
+  }
+}
diff --git a/solr/webapp/web/js/angular/controllers/core-overview.js 
b/solr/webapp/web/js/angular/controllers/core-overview.js
index 7ea076b432a..d71eeed49a8 100644
--- a/solr/webapp/web/js/angular/controllers/core-overview.js
+++ b/solr/webapp/web/js/angular/controllers/core-overview.js
@@ -15,8 +15,15 @@
  limitations under the License.
 */
 
+// follower.leaderDetails comes from an internal v1 call to the leader, so its 
booleans are
+// always the stringified "true"/"false" form, even though the rest of this 
response is v2.
+// TODO: drop this once that internal leader fetch is v2-native too.
+var isTruthy = function(value) {
+    return value === true || value === 'true';
+};
+
 solrAdminApp.controller('CoreOverviewController',
-function($scope, $rootScope, $routeParams, Luke, CoreInfo, Update, 
Replication, Ping, Constants) {
+function($scope, $rootScope, $routeParams, $timeout, Luke, CoreInfo, Update, 
ReplicationV2, Ping, Constants, ApiErrorHandler) {
   $scope.resetMenu("overview", Constants.IS_CORE_PAGE);
   $scope.refreshIndex = function() {
     Luke.index({core: $routeParams.core},
@@ -31,15 +38,24 @@ function($scope, $rootScope, $routeParams, Luke, CoreInfo, 
Update, Replication,
   };
 
   $scope.refreshReplication = function() {
-    Replication.details({core: $routeParams.core},
-      function(data) {
-        $scope.isFollower = data.details.isFollower == "true";
-        $scope.isLeader = data.details.isLeader == "true";
+    ReplicationV2.fetchReplicationDetails($routeParams.core, {}, 
function(error, data, response) {
+      $timeout(function() {
+        if (error) {
+          // An auth failure isn't about replication being unconfigured, so 
route it through the
+          // shared login-redirect/permission-banner handling instead of this 
message.
+          if (response && (response.status === 401 || response.status === 
403)) {
+            $scope.replicationMessage = null;
+            ApiErrorHandler.handle(response);
+          } else {
+            $scope.replicationMessage = "Replication is not configured";
+          }
+          return;
+        }
+        $scope.isFollower = isTruthy(data.details.isFollower);
+        $scope.isLeader = isTruthy(data.details.isLeader);
         $scope.replication = data.details;
-      },
-      function(error) {
-        $scope.replicationMessage = "Replication is not configured";
       });
+    });
   };
 
   $scope.refreshInfo = function() {
diff --git a/solr/webapp/web/js/angular/controllers/replication.js 
b/solr/webapp/web/js/angular/controllers/replication.js
index 6e5fd50086a..a6380701919 100644
--- a/solr/webapp/web/js/angular/controllers/replication.js
+++ b/solr/webapp/web/js/angular/controllers/replication.js
@@ -22,33 +22,37 @@ solrAdminApp.controller('ReplicationController',
         $scope.iterationCount = 1;
 
         $scope.refresh = function() {
-            Replication.details({core:$routeParams.core}, function(response) {
-                var timeout;
-                var interval;
-                if ($scope.interval) $interval.cancel($scope.interval);
-                $scope.isFollower = (response.details.isFollower === 'true');
-                if ($scope.isFollower) {
-                    $scope.progress = 
getProgressDetails(response.details.follower);
-                    $scope.iterations = 
getIterations(response.details.follower);
-                    $scope.versions = getFollowerVersions(response.details);
-                    $scope.settings = getFollowerSettings(response.details);
-                    if ($scope.settings.isReplicating) {
-                        timeout = $timeout($scope.refresh, 1000);
-                    } else if(!$scope.settings.isPollingDisabled && 
$scope.settings.pollInterval) {
-                        interval = $scope.interval = $interval(function() {
-                            $scope.settings.tick--;
-                        }, 1000, $scope.settings.tick);
-                        timeout = $timeout($scope.refresh, 
1000*(1+$scope.settings.tick));
+            ReplicationV2.fetchReplicationDetails($routeParams.core, {}, 
function(error, data, response) {
+                $timeout(function() {
+                    if (error) { ApiErrorHandler.handle(response); return; }
+
+                    var timeout;
+                    var interval;
+                    if ($scope.interval) $interval.cancel($scope.interval);
+                    $scope.isFollower = isTruthy(data.details.isFollower);
+                    if ($scope.isFollower) {
+                        $scope.progress = 
getProgressDetails(data.details.follower);
+                        $scope.iterations = 
getIterations(data.details.follower);
+                        $scope.versions = getFollowerVersions(data.details);
+                        $scope.settings = getFollowerSettings(data.details);
+                        if ($scope.settings.isReplicating) {
+                            timeout = $timeout($scope.refresh, 1000);
+                        } else if(!$scope.settings.isPollingDisabled && 
$scope.settings.pollInterval) {
+                            interval = $scope.interval = $interval(function() {
+                                $scope.settings.tick--;
+                            }, 1000, $scope.settings.tick);
+                            timeout = $timeout($scope.refresh, 
1000*(1+$scope.settings.tick));
+                        }
+                    } else {
+                        $scope.versions = getLeaderVersions(data.details);
                     }
-                } else {
-                    $scope.versions = getLeaderVersions(response.details);
-                }
-                $scope.leader = getLeaderSettings(response.details, 
$scope.isFollower);
+                    $scope.leader = getLeaderSettings(data.details, 
$scope.isFollower);
 
-                var onRouteChangeOff = $scope.$on('$routeChangeStart', 
function() {
-                    if (interval) $interval.cancel(interval);
-                    if (timeout) $timeout.cancel(timeout);
-                    onRouteChangeOff();
+                    var onRouteChangeOff = $scope.$on('$routeChangeStart', 
function() {
+                        if (interval) $interval.cancel(interval);
+                        if (timeout) $timeout.cancel(timeout);
+                        onRouteChangeOff();
+                    });
                 });
             });
 
@@ -64,6 +68,13 @@ solrAdminApp.controller('ReplicationController',
         $scope.refresh();
     });
 
+// follower.leaderDetails comes from an internal v1 call to the leader, so its 
booleans are
+// always the stringified "true"/"false" form, even though the rest of this 
response is v2.
+// TODO: drop this once that internal leader fetch is v2-native too.
+var isTruthy = function(value) {
+    return value === true || value === 'true';
+};
+
 var getProgressDetails = function(progress) {
 
     progress.timeRemaining = parseSeconds(progress.timeRemaining);
@@ -184,9 +195,9 @@ var parseSeconds = function(time) {
 var getFollowerSettings = function(data) {
     var settings = {};
     settings.leaderUrl = data.follower.leaderUrl;
-    settings.isPollingDisabled = data.follower.isPollingDisabled == 'true';
+    settings.isPollingDisabled = isTruthy(data.follower.isPollingDisabled);
     settings.pollInterval = data.follower.pollInterval;
-    settings.isReplicating = data.follower.isReplicating == 'true';
+    settings.isReplicating = isTruthy(data.follower.isReplicating);
     settings.nextExecutionAt = data.follower.nextExecutionAt;
 
     if(settings.isReplicating) {
@@ -209,7 +220,7 @@ var getFollowerSettings = function(data) {
 var getLeaderSettings = function(details, isFollower) {
     var leader = {};
     var leaderData = isFollower ? details.follower.leaderDetails.leader : 
details.leader;
-    leader.replicationEnabled = leaderData.replicationEnabled == "true";
+    leader.replicationEnabled = isTruthy(leaderData.replicationEnabled);
     leader.replicateAfter = leaderData.replicateAfter.join(", ");
 
     if (leaderData.confFiles) {
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 42105030d76..3a85680e9d3 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -17,6 +17,10 @@
 
 var solrAdminServices = angular.module('solrAdminServices', ['ngResource']);
 
+// The generated v2 client caches GET responses by default. Set this here, 
once, since
+// ApiClient.instance is shared across every *V2 factory below.
+solrApi.ApiClient.instance.cache = false;
+
 solrAdminServices.factory('Metrics',
   ['$resource', 'PrometheusParser', function($resource, PrometheusParser) {
     return $resource('admin/metrics', {"wt":"prometheus", "node": "@node", 
"_":Date.now()}, {

Reply via email to