From 51cc09fb4ef20bd6093fdb707eb74166c138bddb Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 23 Jul 2026 16:44:01 -0300 Subject: [PATCH 01/20] Migrate Cores and Segments to using V2 APIs --- .../web/js/angular/controllers/cores.js | 110 +++++++++++------- solr/webapp/web/js/angular/services.js | 8 +- 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/cores.js b/solr/webapp/web/js/angular/controllers/cores.js index 202b6224ca12..7bd0c2c81029 100644 --- a/solr/webapp/web/js/angular/controllers/cores.js +++ b/solr/webapp/web/js/angular/controllers/cores.js @@ -16,35 +16,38 @@ */ solrAdminApp.controller('CoreAdminController', - function($scope, $routeParams, $location, $timeout, $route, Cores, Update, Constants){ + function($scope, $routeParams, $location, $timeout, $route, CoresV2, Update, Constants){ $scope.resetMenu("cores", Constants.IS_ROOT_PAGE); $scope.selectedCore = $routeParams.corename; // use 'corename' not 'core' to distinguish from /solr/:core/ $scope.refresh = function() { - Cores.get(function(data) { - var coreCount = 0; - var cores = data.status; - for (_obj in cores) coreCount++; - $scope.hasCores = coreCount >0; - if (!$scope.selectedCore && coreCount==0) { - $scope.showAddCore(); - return; - } else if (!$scope.selectedCore) { - for (firstCore in cores) break; - $scope.selectedCore = firstCore; - $location.path("/~cores/" + $scope.selectedCore).replace(); - } - $scope.core = cores[$scope.selectedCore]; - $scope.corelist = []; - $scope.swapCorelist = []; - for (var core in cores) { - $scope.corelist.push(cores[core]); - if (cores[core] != $scope.core) { - $scope.swapCorelist.push(cores[core]); + CoresV2.getAllCoreStatus({}, function(error, data, response) { + $timeout(function() { + if (error) return; + var coreCount = 0; + var cores = data.status; + for (_obj in cores) coreCount++; + $scope.hasCores = coreCount >0; + if (!$scope.selectedCore && coreCount==0) { + $scope.showAddCore(); + return; + } else if (!$scope.selectedCore) { + for (firstCore in cores) break; + $scope.selectedCore = firstCore; + $location.path("/~cores/" + $scope.selectedCore).replace(); } - } - if ($scope.swapCorelist.length>0) { - $scope.swapOther = $scope.swapCorelist[0].name; - } + $scope.core = cores[$scope.selectedCore]; + $scope.corelist = []; + $scope.swapCorelist = []; + for (var core in cores) { + $scope.corelist.push(cores[core]); + if (cores[core] != $scope.core) { + $scope.swapCorelist.push(cores[core]); + } + } + if ($scope.swapCorelist.length>0) { + $scope.swapOther = $scope.swapCorelist[0].name; + } + }); }); }; $scope.showAddCore = function() { @@ -67,7 +70,7 @@ solrAdminApp.controller('CoreAdminController', } else if (false) { //@todo detect whether core exists $scope.AddMessage = "A core with that name already exists"; } else { - var params = { + var createCoreParams = { name: $scope.newCore.name, instanceDir: $scope.newCore.instanceDir, config: $scope.newCore.config, @@ -75,12 +78,18 @@ solrAdminApp.controller('CoreAdminController', dataDir: $scope.newCore.dataDir }; if ($scope.isCloud) { - params.collection = $scope.newCore.collection; - params.shard = $scope.newCore.shard; + createCoreParams.collection = $scope.newCore.collection; + createCoreParams.shard = $scope.newCore.shard; } - Cores.add(params, function(data) { - $location.path("/~cores/" + $scope.newCore.name); - $scope.cancelAddCore(); + CoresV2.createCore({createCoreParams: createCoreParams}, function(error, data, response) { + $timeout(function() { + if (error) { + $scope.addMessage = "Error creating core: " + error; + return; + } + $location.path("/~cores/" + $scope.newCore.name); + $scope.cancelAddCore(); + }); }); } }; @@ -93,8 +102,11 @@ solrAdminApp.controller('CoreAdminController', $scope.unloadCore = function() { var answer = confirm( 'Do you really want to unload Core "' + $scope.selectedCore + '"?' ); if( !answer ) return; - Cores.unload({core: $scope.selectedCore}, function(data) { - $location.path("/~cores"); + CoresV2.unloadCore($scope.selectedCore, {}, function(error, data, response) { + $timeout(function() { + if (error) return; + $location.path("/~cores"); + }); }); }; @@ -109,9 +121,15 @@ solrAdminApp.controller('CoreAdminController', } else if ($scope.other == $scope.selectedCore) { $scope.renameMessage = "New name must be different from the current one"; } else { - Cores.rename({core:$scope.selectedCore, other: $scope.other}, function(data) { - $location.path("/~cores/" + $scope.other); - $scope.cancelRename(); + CoresV2.renameCore($scope.selectedCore, {renameCoreRequestBody: {to: $scope.other}}, function(error, data, response) { + $timeout(function() { + if (error) { + $scope.renameMessage = "Error renaming core: " + error; + return; + } + $location.path("/~cores/" + $scope.other); + $scope.cancelRenameCore(); + }); }); } }; @@ -133,10 +151,16 @@ solrAdminApp.controller('CoreAdminController', } else if ($scope.swapOther == $scope.selectedCore) { $scope.swapMessage = "Cannot swap with the same core"; } else { - Cores.swap({core: $scope.selectedCore, other: $scope.swapOther}, function(data) { - $location.path("/~cores/" + $scope.swapOther); - delete $scope.swapOther; - $scope.cancelSwapCores(); + CoresV2.swapCores($scope.selectedCore, {swapCoresRequestBody: {with: $scope.swapOther}}, function(error, data, response) { + $timeout(function() { + if (error) { + $scope.swapMessage = "Error swapping cores: " + error; + return; + } + $location.path("/~cores/" + $scope.swapOther); + delete $scope.swapOther; + $scope.cancelSwapCores(); + }); }); } }; @@ -151,9 +175,9 @@ solrAdminApp.controller('CoreAdminController', delete $scope.initFailures[$scope.selectedCore]; $scope.showInitFailures = Object.keys(data.initFailures).length>0; } - Cores.reload({core: $scope.selectedCore}, - function(data) { - if (data.error) { + CoresV2.reloadCore($scope.selectedCore, + function(error, data, response) { + if (error) { $scope.reloadFailure = true; $timeout(function() { $scope.reloadFailure = false; diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 98e7e37d9baa..b9a151108057 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -50,6 +50,12 @@ solrAdminServices.factory('System', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.CollectionsApi(); }) +.factory('CoresV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.CoresApi(); + }) .factory('Collections', ['$resource', function($resource) { return $resource('admin/collections', @@ -251,7 +257,7 @@ solrAdminServices.factory('System', }]) .factory('Segments', ['$resource', function($resource) { - return $resource(':core/admin/segments', {'wt':'json', core: '@core', _:Date.now()}, { + return $resource('/api/cores/:core/segments', {'wt':'json', core: '@core', _:Date.now()}, { get: {} }); }]) From 9ef7050fcafc551798e56105ef4eb3a7ab3dd7d6 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 23 Jul 2026 16:53:28 -0300 Subject: [PATCH 02/20] Finalize removing Cores in favour of CoresV2 --- solr/webapp/web/js/angular/app.js | 35 ++++++++++--------- .../web/js/angular/controllers/index.js | 2 +- solr/webapp/web/js/angular/services.js | 13 ------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index e4be0017384e..abd74ca4e29b 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -495,7 +495,7 @@ solrAdminApp.config([ }; }); -solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, Cores, Collections, System, Ping, Constants, SchemaDesigner) { +solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, Collections, System, Ping, Constants, SchemaDesigner) { $rootScope.exceptions={}; @@ -516,22 +516,25 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ $scope.refresh(); $scope.resetMenu = function(page, pageType) { - Cores.list(function(data) { - $scope.cores = []; - var currentCoreName = $route.current.params.core; - delete $scope.currentCore; - for (key in data.status) { - var core = data.status[key]; - if (core.name.startsWith("._designer_")) { - continue; - } - $scope.cores.push(core); - if ((!$scope.isSolrCloud || pageType == Constants.IS_CORE_PAGE) && core.name == currentCoreName) { - $scope.currentCore = core; + CoresV2.getAllCoreStatus({indexInfo: false}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.cores = []; + var currentCoreName = $route.current.params.core; + delete $scope.currentCore; + for (key in data.status) { + var core = data.status[key]; + if (core.name.startsWith("._designer_")) { + continue; + } + $scope.cores.push(core); + if ((!$scope.isSolrCloud || pageType == Constants.IS_CORE_PAGE) && core.name == currentCoreName) { + $scope.currentCore = core; + } } - } - $scope.showInitFailures = Object.keys(data.initFailures).length>0; - $scope.initFailures = data.initFailures; + $scope.showInitFailures = Object.keys(data.initFailures).length>0; + $scope.initFailures = data.initFailures; + }); }); System.get(function(data) { diff --git a/solr/webapp/web/js/angular/controllers/index.js b/solr/webapp/web/js/angular/controllers/index.js index eda5ea86c28a..5a70ddcc190d 100644 --- a/solr/webapp/web/js/angular/controllers/index.js +++ b/solr/webapp/web/js/angular/controllers/index.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -solrAdminApp.controller('IndexController', function($scope, System, Cores, Constants) { +solrAdminApp.controller('IndexController', function($scope, System, Constants) { $scope.resetMenu("index", Constants.IS_ROOT_PAGE); $scope.reload = function() { System.get(function(data) { diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index b9a151108057..4c52b73924f2 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -79,19 +79,6 @@ solrAdminServices.factory('System', return $resource('admin/configs', {'wt': 'json', '_': Date.now()}, {"configs": {params: {action: "LIST"}} }); }]) -.factory('Cores', - ['$resource', function($resource) { - return $resource('admin/cores', - {'wt':'json', '_':Date.now()}, { - "query": {}, - "list": {params:{indexInfo: false}}, - "add": {params:{action: "CREATE"}}, - "unload": {params:{action: "UNLOAD", core: "@core"}}, - "rename": {params:{action: "RENAME"}}, - "swap": {params:{action: "SWAP"}}, - "reload": {method: "GET", params:{action:"RELOAD", core: "@core"}, headers:{doNotIntercept: "true"}} - }); - }]) .factory('Logging', ['$resource', function($resource) { return $resource('admin/info/logging', {'wt':'json', '_':Date.now()}, { From a29ef2b62e64d25fb8973a1600ee21de80e4e29b Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 08:18:26 -0300 Subject: [PATCH 03/20] Migrate Logs to v2 except the setLevel, which is v1. We don't yet have the plumbing appaerntly to push the updates to "all" nodes in the V2 api. --- .../web/js/angular/controllers/logging.js | 104 ++++++++++-------- solr/webapp/web/js/angular/services.js | 12 +- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/logging.js b/solr/webapp/web/js/angular/controllers/logging.js index 6b38afe06cec..fcd36e9cf9f8 100644 --- a/solr/webapp/web/js/angular/controllers/logging.js +++ b/solr/webapp/web/js/angular/controllers/logging.js @@ -24,52 +24,55 @@ var format_time_content = function( time, timeZone ) { } solrAdminApp.controller('LoggingController', - function($scope, $timeout, $cookies, Logging, Constants){ + function($scope, $timeout, $cookies, LoggingV2, Constants){ $scope.resetMenu("logging", Constants.IS_ROOT_PAGE); $scope.timezone = $cookies.logging_timezone || "Local"; $scope.refresh = function() { - Logging.events(function(data) { - $scope.since = new Date(); - $scope.sinceDisplay = format_time_content($scope.since, "Local"); - var events = data.history.docs; - for (var i=0; i 1) { - event.trace = event.message; - event.message = lines[0]; + if( !event.trace ) { + var lines = event.message.split( "\n" ); + if( lines.length > 1) { + event.trace = event.message; + event.message = lines[0]; + } } + event.message = event.message.replace(/,/g, ',​'); + event.showTrace = false; } - event.message = event.message.replace(/,/g, ',​'); - event.showTrace = false; - } - $scope.events = events; - $scope.watcher = data.watcher; - /* @todo sticky_mode - // state element is in viewport - sticky_mode = ( state.position().top <= $( window ).scrollTop() + $( window ).height() - ( $( 'body' ).height() - state.position().top ) ); - // initial request - if( 0 === since ) { - sticky_mode = true; - } - $scope.loggingEvents = events; + $scope.events = events; + $scope.watcher = data.watcher; + /* @todo sticky_mode + // state element is in viewport + sticky_mode = ( state.position().top <= $( window ).scrollTop() + $( window ).height() - ( $( 'body' ).height() - state.position().top ) ); + // initial request + if( 0 === since ) { + sticky_mode = true; + } + $scope.loggingEvents = events; - if( sticky_mode ) - { - $( 'body' ) - .animate - ( - { scrollTop: state.position().top }, - 1000 - ); - } - */ + if( sticky_mode ) + { + $( 'body' ) + .animate + ( + { scrollTop: state.position().top }, + 1000 + ); + } + */ + }); }); $scope.timeout = $timeout($scope.refresh, 10000); var onRouteChangeOff = $scope.$on('$routeChangeStart', function() { @@ -98,7 +101,7 @@ solrAdminApp.controller('LoggingController', ) .controller('LoggingLevelController', - function($scope, Logging) { + function($scope, $timeout, Logging, LoggingV2) { $scope.resetMenu("logging-levels"); var packageOf = function(logger) { @@ -123,13 +126,16 @@ solrAdminApp.controller('LoggingController', }; $scope.refresh = function() { - Logging.levels(function(data) { - $scope.logging = makeTree(data.loggers, ""); - $scope.watcher = data.watcher; - $scope.levels = []; - for (level in data.levels) { - $scope.levels.push({name:data.levels[level], pos:level}); - } + LoggingV2.listAllLoggersAndLevels(function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.logging = makeTree(data.loggers, ""); + $scope.watcher = data.watcher; + $scope.levels = []; + for (level in data.levels) { + $scope.levels.push({name:data.levels[level], pos:level}); + } + }); }); }; @@ -155,6 +161,10 @@ solrAdminApp.controller('LoggingController', } var setString = logger.name + ":" + newLevel; logger.showOptions = false; + // Intentionally still v1 (Logging, not LoggingV2): this relies on the "nodes=all" param to + // broadcast the level change to every live node. The v2 NodeLoggingApis endpoint is + // single-node only until SOLR-16738 wires it up to the new V2SolrRequestBasedProxy (see the + // TODO in NodeLogging.java). Move this to LoggingV2.modifyLocalLogLevel once that lands. Logging.setLevel({set: setString}, function(data) { $scope.refresh(); }); diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 4c52b73924f2..6e518dae3ae9 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -56,6 +56,12 @@ solrAdminServices.factory('System', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.CoresApi(); }) +.factory('LoggingV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.LoggingApi(); + }) .factory('Collections', ['$resource', function($resource) { return $resource('admin/collections', @@ -81,9 +87,11 @@ solrAdminServices.factory('System', }]) .factory('Logging', ['$resource', function($resource) { + // "events" and "levels" were migrated to LoggingV2 (see logging.js). This v1 factory is kept + // only for "setLevel", which needs the "nodes=all" broadcast-to-every-node behavior that the + // v2 NodeLoggingApis endpoint doesn't support yet (see SOLR-16738). Retire this factory once + // setLevel moves to LoggingV2 too. return $resource('admin/info/logging', {'wt':'json', '_':Date.now()}, { - "events": {params: {since:'0'}}, - "levels": {}, "setLevel": {params: {nodes:'all'}} }); }]) From f4f6d1b045890aeebf1d79d87f7dc174f78c01ec Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 09:40:29 -0300 Subject: [PATCH 04/20] migrate to v2 for threads, plugins. Plugins used a raw prometheus output, may not wokr. --- solr/webapp/web/js/angular/app.js | 117 ++++++----- .../web/js/angular/controllers/cloud.js | 6 + .../web/js/angular/controllers/index.js | 195 +++++++++--------- .../web/js/angular/controllers/plugins.js | 27 ++- .../web/js/angular/controllers/security.js | 38 ++-- solr/webapp/web/js/angular/services.js | 27 ++- 6 files changed, 221 insertions(+), 189 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index abd74ca4e29b..f7143678d7bb 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -495,7 +495,7 @@ solrAdminApp.config([ }; }); -solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, Collections, System, Ping, Constants, SchemaDesigner) { +solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, Collections, SystemV2, Ping, Constants, SchemaDesigner) { $rootScope.exceptions={}; @@ -537,68 +537,71 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ }); }); - System.get(function(data) { - $scope.isCloudEnabled = data.mode.match( /solrcloud/i ); - $scope.usersPermissions = data.security.permissions; - $scope.isSecurityEnabled = $scope.authenticationPlugin != null; - - $scope.isSchemaDesignerEnabled = $scope.isPermitted([ - permissions.CONFIG_EDIT_PERM, - permissions.SCHEMA_EDIT_PERM, - permissions.READ_PERM, - permissions.UPDATE_PERM - ]); - - var currentCollectionName = $route.current.params.core; - delete $scope.currentCollection; - if ($scope.isCloudEnabled) { - Collections.list(function (cdata) { - Collections.listaliases(function (adata) { - $scope.aliases = []; - for (var key in adata.aliases) { - props = {}; - if (key in adata.properties) { - props = adata.properties[key]; - } - var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; - $scope.aliases.push(alias); - if (pageType == Constants.IS_COLLECTION_PAGE && alias.name == currentCollectionName) { - $scope.currentCollection = alias; - } - } - $scope.collections = []; - for (key in cdata.collections) { - if (cdata.collections[key].startsWith("._designer_")) { - continue; // ignore temp designer collections + SystemV2.getNodeSystemInfo({}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.isCloudEnabled = data.mode.match( /solrcloud/i ); + $scope.usersPermissions = data.security.permissions; + $scope.isSecurityEnabled = $scope.authenticationPlugin != null; + + $scope.isSchemaDesignerEnabled = $scope.isPermitted([ + permissions.CONFIG_EDIT_PERM, + permissions.SCHEMA_EDIT_PERM, + permissions.READ_PERM, + permissions.UPDATE_PERM + ]); + + var currentCollectionName = $route.current.params.core; + delete $scope.currentCollection; + if ($scope.isCloudEnabled) { + Collections.list(function (cdata) { + Collections.listaliases(function (adata) { + $scope.aliases = []; + for (var key in adata.aliases) { + props = {}; + if (key in adata.properties) { + props = adata.properties[key]; + } + var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; + $scope.aliases.push(alias); + if (pageType == Constants.IS_COLLECTION_PAGE && alias.name == currentCollectionName) { + $scope.currentCollection = alias; + } } - var collection = {name: cdata.collections[key], type: 'collection'}; - $scope.collections.push(collection); - if (pageType == Constants.IS_COLLECTION_PAGE && collection.name == currentCollectionName) { - $scope.currentCollection = collection; + $scope.collections = []; + for (key in cdata.collections) { + if (cdata.collections[key].startsWith("._designer_")) { + continue; // ignore temp designer collections + } + var collection = {name: cdata.collections[key], type: 'collection'}; + $scope.collections.push(collection); + if (pageType == Constants.IS_COLLECTION_PAGE && collection.name == currentCollectionName) { + $scope.currentCollection = collection; + } } - } - $scope.aliases_and_collections = $scope.aliases; - if ($scope.aliases.length > 0) { - $scope.aliases_and_collections = $scope.aliases_and_collections.concat({name:'-----'}); - } - $scope.aliases_and_collections = $scope.aliases_and_collections.concat($scope.collections); + $scope.aliases_and_collections = $scope.aliases; + if ($scope.aliases.length > 0) { + $scope.aliases_and_collections = $scope.aliases_and_collections.concat({name:'-----'}); + } + $scope.aliases_and_collections = $scope.aliases_and_collections.concat($scope.collections); + }); }); - }); - } - - $scope.showEnvironment = data.environment !== undefined; - if (data.environment) { - $scope.environment = data.environment; - var env_labels = {'prod': 'Production', 'stage': 'Staging', 'test': 'Test', 'dev': 'Development'}; - $scope.environment_label = env_labels[data.environment]; - if (data.environment_label) { - $scope.environment_label = data.environment_label; } - if (data.environment_color) { - $scope.environment_color = data.environment_color; + + $scope.showEnvironment = data.environment !== undefined; + if (data.environment) { + $scope.environment = data.environment; + var env_labels = {'prod': 'Production', 'stage': 'Staging', 'test': 'Test', 'dev': 'Development'}; + $scope.environment_label = env_labels[data.environment]; + if (data.environment_label) { + $scope.environment_label = data.environment_label; + } + if (data.environment_color) { + $scope.environment_color = data.environment_color; + } } - } + }); }); $scope.showingLogging = page.lastIndexOf("logging", 0) === 0; diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js index f045583ef7f1..454c66c43a5b 100644 --- a/solr/webapp/web/js/angular/controllers/cloud.js +++ b/solr/webapp/web/js/angular/controllers/cloud.js @@ -352,6 +352,12 @@ var nodesSubController = function($scope, Collections, System, Metrics, MetricsE /* Fetch system info for all selected nodes Pick the data we want to display and add it to the node-centric data structure + + Intentionally still v1 (System, not SystemV2): the v2 NodeSystemInfoApi's "nodes" + multi-node proxying (GetNodeSystemInfo.proxyToNodes, via V2SolrRequestBasedProxy) currently + throws a server-side NullPointerException (GetNodeSystemInfo.java, processTypedProxiedResponse) + instead of returning aggregated per-node data. Move this to SystemV2.getNodeSystemInfo once + that's fixed upstream. */ System.get({"nodes": liveNodesToShow.join(',')}, function (systemResponse) { for (var node in systemResponse) { diff --git a/solr/webapp/web/js/angular/controllers/index.js b/solr/webapp/web/js/angular/controllers/index.js index 5a70ddcc190d..60c62b9931eb 100644 --- a/solr/webapp/web/js/angular/controllers/index.js +++ b/solr/webapp/web/js/angular/controllers/index.js @@ -15,111 +15,114 @@ See the License for the specific language governing permissions and limitations under the License. */ -solrAdminApp.controller('IndexController', function($scope, System, Constants) { +solrAdminApp.controller('IndexController', function($scope, $timeout, SystemV2, Constants) { $scope.resetMenu("index", Constants.IS_ROOT_PAGE); $scope.reload = function() { - System.get(function(data) { - $scope.system = data; - const releaseDate = parse_release_date($scope.system.lucene['solr-impl-version']) - $scope.releaseDaysOld = (new Date() - releaseDate)/1000/60/60/24; - - if (data.security.authenticationPlugin) { - $scope.isSecurityEnabled = true - } - - // load average, unless its negative (means n/a on windows, etc) - if (data.system.systemLoadAverage >= 0) { - $scope.load_average = data.system.systemLoadAverage.toFixed(2); - } - - // physical memory - var memoryMax = parse_memory_value(data.system.totalPhysicalMemorySize); - $scope.memoryTotal = parse_memory_value(data.system.totalPhysicalMemorySize - data.system.freePhysicalMemorySize); - $scope.memoryPercentage = ($scope.memoryTotal / memoryMax * 100).toFixed(1)+ "%"; - $scope.memoryMax = pretty_print_bytes(memoryMax); - $scope.memoryTotalDisplay = pretty_print_bytes($scope.memoryTotal); - - // swap space - var swapMax = parse_memory_value(data.system.totalSwapSpaceSize); - $scope.swapTotal = parse_memory_value(data.system.totalSwapSpaceSize - data.system.freeSwapSpaceSize); - $scope.swapPercentage = ($scope.swapTotal / swapMax * 100).toFixed(1)+ "%"; - $scope.swapMax = pretty_print_bytes(swapMax); - $scope.swapTotalDisplay = pretty_print_bytes($scope.swapTotal); - - // file handles - $scope.fileDescriptorPercentage = (data.system.openFileDescriptorCount / data.system.maxFileDescriptorCount *100).toFixed(1) + "%"; - - // java memory - var javaMemoryMax = parse_memory_value(data.jvm.memory.raw.max || data.jvm.memory.max); - $scope.javaMemoryTotal = parse_memory_value(data.jvm.memory.raw.total || data.jvm.memory.total); - $scope.javaMemoryUsed = parse_memory_value(data.jvm.memory.raw.used || data.jvm.memory.used); - $scope.javaMemoryTotalPercentage = ($scope.javaMemoryTotal / javaMemoryMax *100).toFixed(1) + "%"; - $scope.javaMemoryUsedPercentage = ($scope.javaMemoryUsed / $scope.javaMemoryTotal *100).toFixed(1) + "%"; - $scope.javaMemoryPercentage = ($scope.javaMemoryUsed / javaMemoryMax * 100).toFixed(1) + "%"; - $scope.javaMemoryTotalDisplay = pretty_print_bytes($scope.javaMemoryTotal); - $scope.javaMemoryUsedDisplay = pretty_print_bytes($scope.javaMemoryUsed); // @todo These should really be an AngularJS Filter: {{ javaMemoryUsed | bytes }} - $scope.javaMemoryMax = pretty_print_bytes(javaMemoryMax); - - // GPU - $scope.gpuAvailable = data.gpu && data.gpu.available; - if ($scope.gpuAvailable) { - $scope.gpuCount = data.gpu.count; - - var devices = data.gpu.devices; - $scope.gpuDevices = []; - if (devices && Object.keys(devices).length > 0) { - var deviceKeys = Object.keys(devices); - var firstDevice = devices[deviceKeys[0]]; - $scope.gpuName = firstDevice.name; - $scope.gpuId = firstDevice.id; - $scope.gpuCompute = firstDevice.computeCapability; - - if (deviceKeys.length > 1) { - $scope.gpuName += " (+" + (deviceKeys.length - 1) + " more)"; - } + SystemV2.getNodeSystemInfo({}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.system = data; + const releaseDate = parse_release_date($scope.system.lucene['solr-impl-version']) + $scope.releaseDaysOld = (new Date() - releaseDate)/1000/60/60/24; + + if (data.security.authenticationPlugin) { + $scope.isSecurityEnabled = true + } - for (var i = 0; i < deviceKeys.length; i++) { - var device = devices[deviceKeys[i]]; - var gpuData = { - id: device.id, - name: device.name, - computeCapability: device.computeCapability, - totalMemory: device.totalMemory, - usedMemory: device.usedMemory, - freeMemory: device.freeMemory, - active: device.active - }; - - // Add "(active)" indicator to the name for active GPUs - if (gpuData.active) { - gpuData.name += " (active)"; + // load average, unless its negative (means n/a on windows, etc) + if (data.system.systemLoadAverage >= 0) { + $scope.load_average = data.system.systemLoadAverage.toFixed(2); + } + + // physical memory + var memoryMax = parse_memory_value(data.system.totalPhysicalMemorySize); + $scope.memoryTotal = parse_memory_value(data.system.totalPhysicalMemorySize - data.system.freePhysicalMemorySize); + $scope.memoryPercentage = ($scope.memoryTotal / memoryMax * 100).toFixed(1)+ "%"; + $scope.memoryMax = pretty_print_bytes(memoryMax); + $scope.memoryTotalDisplay = pretty_print_bytes($scope.memoryTotal); + + // swap space + var swapMax = parse_memory_value(data.system.totalSwapSpaceSize); + $scope.swapTotal = parse_memory_value(data.system.totalSwapSpaceSize - data.system.freeSwapSpaceSize); + $scope.swapPercentage = ($scope.swapTotal / swapMax * 100).toFixed(1)+ "%"; + $scope.swapMax = pretty_print_bytes(swapMax); + $scope.swapTotalDisplay = pretty_print_bytes($scope.swapTotal); + + // file handles + $scope.fileDescriptorPercentage = (data.system.openFileDescriptorCount / data.system.maxFileDescriptorCount *100).toFixed(1) + "%"; + + // java memory + var javaMemoryMax = parse_memory_value(data.jvm.memory.raw.max || data.jvm.memory.max); + $scope.javaMemoryTotal = parse_memory_value(data.jvm.memory.raw.total || data.jvm.memory.total); + $scope.javaMemoryUsed = parse_memory_value(data.jvm.memory.raw.used || data.jvm.memory.used); + $scope.javaMemoryTotalPercentage = ($scope.javaMemoryTotal / javaMemoryMax *100).toFixed(1) + "%"; + $scope.javaMemoryUsedPercentage = ($scope.javaMemoryUsed / $scope.javaMemoryTotal *100).toFixed(1) + "%"; + $scope.javaMemoryPercentage = ($scope.javaMemoryUsed / javaMemoryMax * 100).toFixed(1) + "%"; + $scope.javaMemoryTotalDisplay = pretty_print_bytes($scope.javaMemoryTotal); + $scope.javaMemoryUsedDisplay = pretty_print_bytes($scope.javaMemoryUsed); // @todo These should really be an AngularJS Filter: {{ javaMemoryUsed | bytes }} + $scope.javaMemoryMax = pretty_print_bytes(javaMemoryMax); + + // GPU + $scope.gpuAvailable = data.gpu && data.gpu.available; + if ($scope.gpuAvailable) { + $scope.gpuCount = data.gpu.count; + + var devices = data.gpu.devices; + $scope.gpuDevices = []; + if (devices && Object.keys(devices).length > 0) { + var deviceKeys = Object.keys(devices); + var firstDevice = devices[deviceKeys[0]]; + $scope.gpuName = firstDevice.name; + $scope.gpuId = firstDevice.id; + $scope.gpuCompute = firstDevice.computeCapability; + + if (deviceKeys.length > 1) { + $scope.gpuName += " (+" + (deviceKeys.length - 1) + " more)"; } - // Only calculate memory display for active GPUs - if (gpuData.active && gpuData.totalMemory && gpuData.usedMemory) { - var total = parse_memory_value(gpuData.totalMemory); - var used = parse_memory_value(gpuData.usedMemory); - gpuData.memoryPercentage = (used / total * 100).toFixed(1) + "%"; - gpuData.totalMemoryDisplay = pretty_print_bytes(total); - gpuData.usedMemoryDisplay = pretty_print_bytes(used); + for (var i = 0; i < deviceKeys.length; i++) { + var device = devices[deviceKeys[i]]; + var gpuData = { + id: device.id, + name: device.name, + computeCapability: device.computeCapability, + totalMemory: device.totalMemory, + usedMemory: device.usedMemory, + freeMemory: device.freeMemory, + active: device.active + }; + + // Add "(active)" indicator to the name for active GPUs + if (gpuData.active) { + gpuData.name += " (active)"; + } + + // Only calculate memory display for active GPUs + if (gpuData.active && gpuData.totalMemory && gpuData.usedMemory) { + var total = parse_memory_value(gpuData.totalMemory); + var used = parse_memory_value(gpuData.usedMemory); + gpuData.memoryPercentage = (used / total * 100).toFixed(1) + "%"; + gpuData.totalMemoryDisplay = pretty_print_bytes(total); + gpuData.usedMemoryDisplay = pretty_print_bytes(used); + } + $scope.gpuDevices.push(gpuData); } - $scope.gpuDevices.push(gpuData); } } - } - - // no info bar: - $scope.noInfo = !( - data.system.totalPhysicalMemorySize && data.system.freePhysicalMemorySize && - data.system.totalSwapSpaceSize && data.system.freeSwapSpaceSize && - data.system.openFileDescriptorCount && data.system.maxFileDescriptorCount); - - // save a copy of the original commandline args - $scope.commandLineArgsUnsorted = [...data.jvm.jmx.commandLineArgs]; - // get commandline args latest orderby or defaults to "Unsorted" - $scope.commandLineOrderBy = sessionStorage.getItem("commandline.orderby") || "Unsorted"; - $scope.showCommandLineArgs(); + + // no info bar: + $scope.noInfo = !( + data.system.totalPhysicalMemorySize && data.system.freePhysicalMemorySize && + data.system.totalSwapSpaceSize && data.system.freeSwapSpaceSize && + data.system.openFileDescriptorCount && data.system.maxFileDescriptorCount); + + // save a copy of the original commandline args + $scope.commandLineArgsUnsorted = [...data.jvm.jmx.commandLineArgs]; + // get commandline args latest orderby or defaults to "Unsorted" + $scope.commandLineOrderBy = sessionStorage.getItem("commandline.orderby") || "Unsorted"; + $scope.showCommandLineArgs(); }); + }); }; $scope.toggleCommandLineOrder = function() { $scope.commandLineOrderBy = ($scope.commandLineOrderBy=="Sorted") ? "Unsorted":"Sorted"; diff --git a/solr/webapp/web/js/angular/controllers/plugins.js b/solr/webapp/web/js/angular/controllers/plugins.js index bf0dd9bcac73..b9561a61ec99 100644 --- a/solr/webapp/web/js/angular/controllers/plugins.js +++ b/solr/webapp/web/js/angular/controllers/plugins.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('PluginsController', - function($scope, $rootScope, $routeParams, $location, Metrics, Constants) { + function($scope, $rootScope, $routeParams, $location, $timeout, MetricsV2, Constants) { $scope.resetMenu("plugins", Constants.IS_CORE_PAGE); if ($routeParams.legacytype) { @@ -34,16 +34,21 @@ solrAdminApp.controller('PluginsController', var type = $location.search().type; - Metrics.raw(params, function (response) { - $scope.types = getPluginTypesFromMetrics(response.data, type); - $scope.type = getSelectedType($scope.types, type); - - if ($scope.type && $routeParams.entry) { - $scope.plugins = $routeParams.entry.split(","); - openPlugins($scope.type, $scope.plugins); - } else { - $scope.plugins = []; - } + // getMetrics always returns null for "data" (its returnType is null in the generated + // client); the raw Prometheus-format text response body lives on "response.text". + MetricsV2.getMetrics(params, function (error, data, response) { + $timeout(function() { + if (error) return; + $scope.types = getPluginTypesFromMetrics(response.text, type); + $scope.type = getSelectedType($scope.types, type); + + if ($scope.type && $routeParams.entry) { + $scope.plugins = $routeParams.entry.split(","); + openPlugins($scope.type, $scope.plugins); + } else { + $scope.plugins = []; + } + }); }); }; diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js index 776116eedd52..7ccfd2e0908b 100644 --- a/solr/webapp/web/js/angular/controllers/security.js +++ b/solr/webapp/web/js/angular/controllers/security.js @@ -15,7 +15,7 @@ limitations under the License. */ -solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cookies, $window, Constants, System, Security) { +solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cookies, $window, Constants, SystemV2, Security) { $scope.resetMenu("security", Constants.IS_ROOT_PAGE); $scope.params = []; @@ -276,21 +276,25 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki $scope.permFilterOptions = []; $scope.permFilterTypes = ["", "name", "role", "path", "collection"]; - System.get(function(data) { - $scope.tls = data.security ? data.security["tls"] : false; - $scope.authenticationPlugin = data.security ? data.security["authenticationPlugin"] : null; - $scope.authorizationPlugin = data.security ? data.security["authorizationPlugin"] : null; - $scope.isSecurityAdminEnabled = $scope.authenticationPlugin != null; - $scope.isCloudMode = data.mode.match( /solrcloud/i ) != null; - $scope.zkHost = $scope.isCloudMode ? data["zkHost"] : ""; - $scope.solrHome = data["solr_home"]; - $scope.refreshSecurityPanel(); - }, function(e) { - if (e.status === 401 || e.status === 403) { - $scope.isSecurityAdminEnabled = true; - $scope.hasSecurityEditPerm = false; - $scope.hideAll(); - } + SystemV2.getNodeSystemInfo({}, function(error, data, response) { + $timeout(function() { + if (error) { + if (response && (response.status === 401 || response.status === 403)) { + $scope.isSecurityAdminEnabled = true; + $scope.hasSecurityEditPerm = false; + $scope.hideAll(); + } + return; + } + $scope.tls = data.security ? data.security["tls"] : false; + $scope.authenticationPlugin = data.security ? data.security["authenticationPlugin"] : null; + $scope.authorizationPlugin = data.security ? data.security["authorizationPlugin"] : null; + $scope.isSecurityAdminEnabled = $scope.authenticationPlugin != null; + $scope.isCloudMode = data.mode.match( /solrcloud/i ) != null; + $scope.zkHost = $scope.isCloudMode ? data["zkHost"] : ""; + $scope.solrHome = data["solr_home"]; + $scope.refreshSecurityPanel(); + }); }); }; @@ -355,7 +359,7 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki // check for issues with perm config $scope.validatePermConfig(); - // use the current user's roles (obtained from System.get) to check if they have the security permissions + // use the current user's roles (obtained from SystemV2.getNodeSystemInfo) to check if they have the security permissions // Note: the backend will check too so this is only for display purposes $scope.hasSecurityEditPerm = $scope.isPermitted(permissions.SECURITY_EDIT_PERM); $scope.hasSecurityReadPerm = $scope.hasSecurityEditPerm || $scope.isPermitted(permissions.SECURITY_READ_PERM); diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 6e518dae3ae9..1c8bb42918ae 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -23,6 +23,8 @@ solrAdminServices.factory('System', }]) .factory('Metrics', ['$resource', 'PrometheusParser', function($resource, PrometheusParser) { + // "raw" was migrated to MetricsV2 (see plugins.js). This v1 factory is kept only for "get", + // still used by cloud.js's per-node metrics fetch. return $resource('admin/metrics', {"wt":"prometheus", "node": "@node", "_":Date.now()}, { get: { method: 'GET', @@ -34,13 +36,6 @@ solrAdminServices.factory('System', return {metrics: {}, error: e.message}; } } - }, - "raw": { - method: 'GET', - params: {wt: 'prometheus', core: '@core'}, - transformResponse: function(data) { - return {data: data}; - } } }); }]) @@ -62,6 +57,18 @@ solrAdminServices.factory('System', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.LoggingApi(); }) +.factory('SystemV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.SystemApi(); + }) +.factory('MetricsV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.MetricsApi(); + }) .factory('Collections', ['$resource', function($resource) { return $resource('admin/collections', @@ -116,7 +123,11 @@ solrAdminServices.factory('System', }]) .factory('Threads', ['$resource', function($resource) { - return $resource('admin/info/threads', {'wt':'json', '_':Date.now()}); + // v2 NodeThreadsAPI (/api/node/threads) still just delegates straight through to the same v1 + // ThreadDumpHandler, so the response shape is byte-identical -- no generated solrApi client + // class exists for it (it predates the OpenAPI-based v2 API framework), so this stays a plain + // $resource, like SchemaDesigner/Security/Segments. + return $resource('/api/node/threads', {'wt':'json', '_':Date.now()}); }]) .factory('Properties', ['$resource', function($resource) { From 194e9c78baa087cb5839fbad465299917884017c Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 09:47:16 -0300 Subject: [PATCH 05/20] Back out metrics chnages (and plugins.js), they need more investigagion. --- .../web/js/angular/controllers/plugins.js | 27 ++++++++----------- solr/webapp/web/js/angular/services.js | 15 +++++------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/plugins.js b/solr/webapp/web/js/angular/controllers/plugins.js index b9561a61ec99..bf0dd9bcac73 100644 --- a/solr/webapp/web/js/angular/controllers/plugins.js +++ b/solr/webapp/web/js/angular/controllers/plugins.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('PluginsController', - function($scope, $rootScope, $routeParams, $location, $timeout, MetricsV2, Constants) { + function($scope, $rootScope, $routeParams, $location, Metrics, Constants) { $scope.resetMenu("plugins", Constants.IS_CORE_PAGE); if ($routeParams.legacytype) { @@ -34,21 +34,16 @@ solrAdminApp.controller('PluginsController', var type = $location.search().type; - // getMetrics always returns null for "data" (its returnType is null in the generated - // client); the raw Prometheus-format text response body lives on "response.text". - MetricsV2.getMetrics(params, function (error, data, response) { - $timeout(function() { - if (error) return; - $scope.types = getPluginTypesFromMetrics(response.text, type); - $scope.type = getSelectedType($scope.types, type); - - if ($scope.type && $routeParams.entry) { - $scope.plugins = $routeParams.entry.split(","); - openPlugins($scope.type, $scope.plugins); - } else { - $scope.plugins = []; - } - }); + Metrics.raw(params, function (response) { + $scope.types = getPluginTypesFromMetrics(response.data, type); + $scope.type = getSelectedType($scope.types, type); + + if ($scope.type && $routeParams.entry) { + $scope.plugins = $routeParams.entry.split(","); + openPlugins($scope.type, $scope.plugins); + } else { + $scope.plugins = []; + } }); }; diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 1c8bb42918ae..a47940de0df5 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -23,8 +23,6 @@ solrAdminServices.factory('System', }]) .factory('Metrics', ['$resource', 'PrometheusParser', function($resource, PrometheusParser) { - // "raw" was migrated to MetricsV2 (see plugins.js). This v1 factory is kept only for "get", - // still used by cloud.js's per-node metrics fetch. return $resource('admin/metrics', {"wt":"prometheus", "node": "@node", "_":Date.now()}, { get: { method: 'GET', @@ -36,6 +34,13 @@ solrAdminServices.factory('System', return {metrics: {}, error: e.message}; } } + }, + "raw": { + method: 'GET', + params: {wt: 'prometheus', core: '@core'}, + transformResponse: function(data) { + return {data: data}; + } } }); }]) @@ -63,12 +68,6 @@ solrAdminServices.factory('System', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.SystemApi(); }) -.factory('MetricsV2', - function() { - solrApi.ApiClient.instance.basePath = '/api'; - delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; - return new solrApi.MetricsApi(); - }) .factory('Collections', ['$resource', function($resource) { return $resource('admin/collections', From b206b736c44dcb861c110e489a23ca608057b042 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 10:18:58 -0300 Subject: [PATCH 06/20] Migrate all of Collections to V2, except ClusterStatus which appears to not have a v2 equivalent. --- .../web/js/angular/controllers/collections.js | 153 ++++++++++-------- solr/webapp/web/js/angular/services.js | 51 ++++-- 2 files changed, 121 insertions(+), 83 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js index 830290a18069..9e4a23026270 100644 --- a/solr/webapp/web/js/angular/controllers/collections.js +++ b/solr/webapp/web/js/angular/controllers/collections.js @@ -16,19 +16,18 @@ */ solrAdminApp.controller('CollectionsController', - function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, Zookeeper, Constants, ConfigSets){ + function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants){ $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $scope.refresh = function() { $scope.rootUrl = Constants.ROOT_URL + "#/~collections/" + $routeParams.collection; - Zookeeper.liveNodes({}, function(data) { - $scope.availableNodeSet = []; - var children = data.tree[0].children; - for (var child in children) { - $scope.availableNodeSet.push(children[child].text); - } + ClusterV2.listClusterNodes(function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.availableNodeSet = data.nodes; + }); }); Collections.status(function (data) { @@ -62,36 +61,42 @@ solrAdminApp.controller('CollectionsController', $scope.collection = collection; } } - // Fetch aliases using LISTALIASES to get properties - Collections.listaliases(function (adata) { - // TODO: Population of aliases array duplicated in app.js - $scope.aliases = []; - for (var key in adata.aliases) { - props = {}; - if (key in adata.properties) { - props = adata.properties[key]; + // Fetch aliases using getAliases to get properties + AliasesV2.getAliases(function (error, adata, response) { + $timeout(function() { + if (error) return; + // TODO: Population of aliases array duplicated in app.js + $scope.aliases = []; + for (var key in adata.aliases) { + props = {}; + if (key in adata.properties) { + props = adata.properties[key]; + } + var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; + $scope.aliases.push(alias); + if ($routeParams.collection == 'alias_' + key) { + $scope.collection = alias; + } } - var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; - $scope.aliases.push(alias); - if ($routeParams.collection == 'alias_' + key) { - $scope.collection = alias; + // Decide what is selected in list + if ($routeParams.collection && !$scope.collection) { + alert("No collection or alias called " + $routeParams.collection); + $location.path("/~collections"); } - } - // Decide what is selected in list - if ($routeParams.collection && !$scope.collection) { - alert("No collection or alias called " + $routeParams.collection); - $location.path("/~collections"); - } + }); }); $scope.liveNodes = data.cluster.liveNodes; }); - ConfigSets.configs(function(data) { - $scope.configs = []; - var items = data.configSets; - for (var i in items) { - $scope.configs.push({name: items[i]}); - } + ConfigSetsV2.listConfigSet(function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.configs = []; + var items = data.configSets; + for (var i in items) { + $scope.configs.push({name: items[i]}); + } + }); }); }; @@ -138,17 +143,23 @@ solrAdminApp.controller('CollectionsController', for (var i in $scope.aliasCollections) { collections.push($scope.aliasCollections[i].name); } - Collections.createAlias({name: $scope.aliasToCreate, collections: collections.join(",")}, function(data) { - $scope.cancelCreateAlias(); - $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); - $location.path("/~collections/alias_" + $scope.aliasToCreate); + AliasesV2.createAlias({createAliasRequestBody: {name: $scope.aliasToCreate, collections: collections}}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.cancelCreateAlias(); + $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); + $location.path("/~collections/alias_" + $scope.aliasToCreate); + }); }); } $scope.deleteAlias = function() { - Collections.deleteAlias({name: $scope.collection.name}, function(data) { - $scope.hideAll(); - $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); - $location.path("/~collections/"); + AliasesV2.deleteAlias($scope.collection.name, {}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.hideAll(); + $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); + $location.path("/~collections/"); + }); }); }; @@ -162,26 +173,29 @@ solrAdminApp.controller('CollectionsController', $scope.addMessage = "A collection can't be made up of just PULL replicas"; } else { var coll = $scope.newCollection; - var params = { + var createCollectionParams = { name: coll.name, - "router.name": coll.routerName, + router: {name: coll.routerName}, numShards: coll.numShards, - "collection.configName": coll.configName + config: coll.configName }; - if (coll.shards) params.shards = coll.shards; - if (coll.routerField) params["router.field"] = coll.routerField; - if (coll.createNodeSet) params.createNodeSet = coll.createNodeSet.join(","); + if (coll.shards) createCollectionParams.shardNames = coll.shards.split(",").map(function(s) { return s.trim(); }); + if (coll.routerField) createCollectionParams.router.field = coll.routerField; + if (coll.createNodeSet) createCollectionParams.nodeSet = coll.createNodeSet; if ($scope.replicaTypesChosen()) { - params["nrtReplicas"] = coll.nrtReplicas; - params["tlogReplicas"] = coll.tlogReplicas; - params["pullReplicas"] = coll.pullReplicas; + createCollectionParams.nrtReplicas = coll.nrtReplicas; + createCollectionParams.tlogReplicas = coll.tlogReplicas; + createCollectionParams.pullReplicas = coll.pullReplicas; } else { - params["replicationFactor"] = coll.replicationFactor; + createCollectionParams.replicationFactor = coll.replicationFactor; } - Collections.add(params, function(data) { - $scope.cancelAddCollection(); - $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); - $location.path("/~collections/" + $scope.newCollection.name); + CollectionsV2.createCollection({createCollectionRequestBody: createCollectionParams}, function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.cancelAddCollection(); + $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); + $location.path("/~collections/" + $scope.newCollection.name); + }); }); } }; @@ -202,8 +216,11 @@ solrAdminApp.controller('CollectionsController', $scope.deleteCollection = function() { if ($scope.collection.name == $scope.collectionDeleteConfirm) { - Collections.delete({name: $scope.collection.name}, function (data) { - $location.path("/~collections"); + CollectionsV2.deleteCollection($scope.collection.name, {}, function (error, data, response) { + $timeout(function() { + if (error) return; + $location.path("/~collections"); + }); }); } else { $scope.deleteMessage = "Collection names do not match."; @@ -232,12 +249,11 @@ solrAdminApp.controller('CollectionsController', shard.showAdd = !shard.showAdd; delete $scope.addReplicaMessage; - Zookeeper.liveNodes({}, function(data) { - $scope.nodes = []; - var children = data.tree[0].children; - for (var child in children) { - $scope.nodes.push(children[child].text); - } + ClusterV2.listClusterNodes(function(error, data, response) { + $timeout(function() { + if (error) return; + $scope.nodes = data.nodes; + }); }); }; @@ -252,7 +268,8 @@ solrAdminApp.controller('CollectionsController', }; $scope.deleteShard = function(shard) { - Collections.deleteShard({collection: shard.collection, shard:shard.name}, function(data) { + ShardsV2.deleteShard(shard.collection, shard.name, {}, function(error, data, response) { + if (error) return; shard.deleted = true; $timeout(function() { $scope.refresh(); @@ -261,7 +278,8 @@ solrAdminApp.controller('CollectionsController', } $scope.deleteReplica = function(replica) { - Collections.deleteReplica({collection: replica.collection, shard:replica.shard, replica:replica.name}, function(data) { + ReplicasV2.deleteReplicaByName(replica.collection, replica.shard, replica.name, {}, function(error, data, response) { + if (error) return; replica.deleted = true; $timeout(function() { $scope.refresh(); @@ -269,15 +287,14 @@ solrAdminApp.controller('CollectionsController', }); } $scope.addReplica = function(shard) { - var params = { - collection: shard.collection, - shard: shard.name, + var createReplicaParams = { type: shard.replicaType } if (shard.replicaNodeName && shard.replicaNodeName != "") { - params.node = shard.replicaNodeName; + createReplicaParams.node = shard.replicaNodeName; } - Collections.addReplica(params, function(data) { + ReplicasV2.createReplica(shard.collection, shard.name, {createReplicaRequestBody: createReplicaParams}, function(error, data, response) { + if (error) return; shard.replicaAdded = true; $timeout(function () { shard.replicaAdded = false; diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index a47940de0df5..73dcf3ea4277 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -68,29 +68,50 @@ solrAdminServices.factory('System', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.SystemApi(); }) +.factory('AliasesV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.AliasesApi(); + }) +.factory('ShardsV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.ShardsApi(); + }) +.factory('ReplicasV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.ReplicasApi(); + }) +.factory('ConfigSetsV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.ConfigsetsApi(); + }) +.factory('ClusterV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.ClusterApi(); + }) .factory('Collections', ['$resource', function($resource) { + // "add", "delete", "createAlias", "deleteAlias", "deleteReplica", "addReplica", "deleteShard" + // were migrated to CollectionsV2/AliasesV2/ShardsV2/ReplicasV2 (see collections.js). This v1 + // factory is kept for "list"/"listaliases" (still used by app.js's nav-list population) and + // "status"/CLUSTERSTATUS (used by collections.js, cloud.js, collection-overview.js -- no + // single-request v2 equivalent exists yet; see the collections.js migration discussion). return $resource('admin/collections', {'wt':'json', '_':Date.now()}, { "list": {params:{action: "LIST"}}, "listaliases": {params:{action: "LISTALIASES"}}, - "status": {params:{action: "CLUSTERSTATUS"}}, - "add": {params:{action: "CREATE"}}, - "delete": {params:{action: "DELETE"}}, - "rename": {params:{action: "RENAME"}}, - "createAlias": {params:{action: "CREATEALIAS"}}, - "deleteAlias": {params:{action: "DELETEALIAS"}}, - "deleteReplica": {params:{action: "DELETEREPLICA"}}, - "addReplica": {params:{action: "ADDREPLICA"}}, - "deleteShard": {params:{action: "DELETESHARD"}}, - "reload": {method: "GET", params:{action:"RELOAD", core: "@core"}} + "status": {params:{action: "CLUSTERSTATUS"}} }); }]) -.factory('ConfigSets', - ['$resource', function ($resource) { - return $resource('admin/configs', {'wt': 'json', '_': Date.now()}, {"configs": {params: {action: "LIST"}} - }); - }]) .factory('Logging', ['$resource', function($resource) { // "events" and "levels" were migrated to LoggingV2 (see logging.js). This v1 factory is kept From 4971a27ca9182ef6510563d1b9bc44e355bc6314 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 10:49:21 -0300 Subject: [PATCH 07/20] Migrate Paramsets to v2 --- solr/webapp/web/js/angular/controllers/paramsets.js | 8 +++++++- solr/webapp/web/js/angular/services.js | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/paramsets.js b/solr/webapp/web/js/angular/controllers/paramsets.js index e18870b3ce3e..a11dad23fb3f 100644 --- a/solr/webapp/web/js/angular/controllers/paramsets.js +++ b/solr/webapp/web/js/angular/controllers/paramsets.js @@ -108,6 +108,13 @@ solrAdminApp.controller('ParamSetsController', $scope.refresh(); $scope.submit = function () { + if (!$scope.paramsetContent || !$scope.paramsetContent.trim()) { + $scope.responseStatus = "error"; + $scope.response = "Please enter a Paramset(s) JSON payload before submitting. " + + "(Checking \"Sample Paramset\" only shows an example -- copy it into the text area to use it.)"; + return; + } + var params = {}; params.core = $routeParams.core; @@ -134,7 +141,6 @@ solrAdminApp.controller('ParamSetsController', params.core = $routeParams.core; params.wt = "json"; - params.name = $scope.name; var apiPayload = { "delete": [$scope.name] diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 73dcf3ea4277..2032becbee89 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -176,7 +176,15 @@ solrAdminServices.factory('System', }]) .factory('ParamSet', ['$resource', function($resource) { - return $resource(':core/config/params/:name', {core: '@core', wt:'json', _:Date.now()}, { + // v2 GetConfigAPI/ModifyParamSetAPI (/api/collections/:core/config/params) still delegate + // straight through to the same v1 SolrConfigHandler, so the response shape is byte-identical + // -- no generated solrApi client class exists for it (old-style @EndPoint API, predates the + // OpenAPI-based v2 framework), so this stays a plain $resource, like Segments/Threads. + // NB: despite the "core" param name (kept for template/route compatibility), this must be a + // *collection* name -- the paramsets nav link is built from currentCollection.name (see + // index.html), since standalone mode no longer exists and cloud mode's v2 API distinguishes + // /cores/{coreName}/... from /collections/{collectionName}/... unlike v1's flexible routing. + return $resource('/api/collections/:core/config/params/:name', {core: '@core', wt:'json', _:Date.now()}, { "submit": {headers: {'Content-type': 'application/json'}, method: "POST"}, "get": {headers: {'Content-type': 'application/json'}, method: "GET"} }); From becbe1f9713544d520a2df66db6be4291e753949 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 11:31:27 -0300 Subject: [PATCH 08/20] Ensure standalone/user-managed still works. --- .../web/js/angular/controllers/paramsets.js | 11 ++++++ .../web/js/angular/controllers/query.js | 3 ++ solr/webapp/web/js/angular/services.js | 36 +++++++++---------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/paramsets.js b/solr/webapp/web/js/angular/controllers/paramsets.js index a11dad23fb3f..79ce7c53312c 100644 --- a/solr/webapp/web/js/angular/controllers/paramsets.js +++ b/solr/webapp/web/js/angular/controllers/paramsets.js @@ -33,6 +33,13 @@ solrAdminApp.controller('ParamSetsController', } } + // The v2 config/params API needs to know up front whether ":core" is a collection name + // (SolrCloud) or an actual core name (standalone/user-managed) -- there's no equivalent of + // v1's flexible core-or-collection routing. + $scope.paramSetIndexType = function() { + return $scope.isCloudEnabled ? "collections" : "cores"; + } + $scope.selectParamset = function() { $location.search("paramset", $scope.name); $scope.getParamset($scope.name); @@ -43,6 +50,7 @@ solrAdminApp.controller('ParamSetsController', var params = {}; params.core = $routeParams.core; + params.indexType = $scope.paramSetIndexType(); params.wt = "json"; params.name = paramset; @@ -76,6 +84,7 @@ solrAdminApp.controller('ParamSetsController', var params = {}; params.core = $routeParams.core; + params.indexType = $scope.paramSetIndexType(); params.wt = "json"; ParamSet.get(params, callback, failure); @@ -118,6 +127,7 @@ solrAdminApp.controller('ParamSetsController', var params = {}; params.core = $routeParams.core; + params.indexType = $scope.paramSetIndexType(); params.wt = "json"; ParamSet.submit(params, $scope.paramsetContent, callback, failure); @@ -140,6 +150,7 @@ solrAdminApp.controller('ParamSetsController', var params = {}; params.core = $routeParams.core; + params.indexType = $scope.paramSetIndexType(); params.wt = "json"; var apiPayload = { diff --git a/solr/webapp/web/js/angular/controllers/query.js b/solr/webapp/web/js/angular/controllers/query.js index 33294b810972..5c8532cf1a74 100644 --- a/solr/webapp/web/js/angular/controllers/query.js +++ b/solr/webapp/web/js/angular/controllers/query.js @@ -36,6 +36,9 @@ solrAdminApp.controller('QueryController', var params = {}; params.core = $routeParams.core; + // The v2 config/params API needs to know up front whether ":core" is a collection name + // (SolrCloud) or an actual core name (standalone/user-managed); see paramsets.js for details. + params.indexType = $scope.isCloudEnabled ? "collections" : "cores"; params.wt = "json"; ParamSet.get(params, callback, failure); diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 2032becbee89..9d4f3bd980b0 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -100,11 +100,10 @@ solrAdminServices.factory('System', }) .factory('Collections', ['$resource', function($resource) { - // "add", "delete", "createAlias", "deleteAlias", "deleteReplica", "addReplica", "deleteShard" - // were migrated to CollectionsV2/AliasesV2/ShardsV2/ReplicasV2 (see collections.js). This v1 - // factory is kept for "list"/"listaliases" (still used by app.js's nav-list population) and - // "status"/CLUSTERSTATUS (used by collections.js, cloud.js, collection-overview.js -- no - // single-request v2 equivalent exists yet; see the collections.js migration discussion). + // This v1 factory only covers "list"/"listaliases" (used by app.js's nav-list population) and + // "status"/CLUSTERSTATUS (used by collections.js, cloud.js, collection-overview.js) -- no + // single-request v2 equivalent for CLUSTERSTATUS exists yet, and no other actions here have + // any remaining callers. return $resource('admin/collections', {'wt':'json', '_':Date.now()}, { "list": {params:{action: "LIST"}}, @@ -114,10 +113,9 @@ solrAdminServices.factory('System', }]) .factory('Logging', ['$resource', function($resource) { - // "events" and "levels" were migrated to LoggingV2 (see logging.js). This v1 factory is kept - // only for "setLevel", which needs the "nodes=all" broadcast-to-every-node behavior that the - // v2 NodeLoggingApis endpoint doesn't support yet (see SOLR-16738). Retire this factory once - // setLevel moves to LoggingV2 too. + // This v1 factory only covers "setLevel", which needs the "nodes=all" broadcast-to-every-node + // behavior that the v2 NodeLoggingApis endpoint doesn't support yet (see SOLR-16738). Retire + // this factory once setLevel moves to LoggingV2. return $resource('admin/info/logging', {'wt':'json', '_':Date.now()}, { "setLevel": {params: {nodes:'all'}} }); @@ -176,15 +174,17 @@ solrAdminServices.factory('System', }]) .factory('ParamSet', ['$resource', function($resource) { - // v2 GetConfigAPI/ModifyParamSetAPI (/api/collections/:core/config/params) still delegate - // straight through to the same v1 SolrConfigHandler, so the response shape is byte-identical - // -- no generated solrApi client class exists for it (old-style @EndPoint API, predates the - // OpenAPI-based v2 framework), so this stays a plain $resource, like Segments/Threads. - // NB: despite the "core" param name (kept for template/route compatibility), this must be a - // *collection* name -- the paramsets nav link is built from currentCollection.name (see - // index.html), since standalone mode no longer exists and cloud mode's v2 API distinguishes - // /cores/{coreName}/... from /collections/{collectionName}/... unlike v1's flexible routing. - return $resource('/api/collections/:core/config/params/:name', {core: '@core', wt:'json', _:Date.now()}, { + // v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still + // delegate straight through to the same v1 SolrConfigHandler, so the response shape is + // byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API, + // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like + // Segments/Threads. + // NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a + // collection name (SolrCloud) or an actual core name (standalone/user-managed) -- + // /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK), + // and there's no such thing as a "collection" there anyway. Callers must pass "indexType" as + // "collections" or "cores" (see paramsets.js, driven by $scope.isCloudEnabled). + return $resource('/api/:indexType/:core/config/params/:name', {core: '@core', indexType: '@indexType', wt:'json', _:Date.now()}, { "submit": {headers: {'Content-type': 'application/json'}, method: "POST"}, "get": {headers: {'Content-type': 'application/json'}, method: "GET"} }); From be1e11f3924a562c47956948e131babab706675f Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 11:39:46 -0300 Subject: [PATCH 09/20] Two lingering bugs in the Admin UI that reviewing htis PR flagged. --- solr/webapp/web/js/angular/controllers/cores.js | 2 +- solr/webapp/web/js/angular/services.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/cores.js b/solr/webapp/web/js/angular/controllers/cores.js index 7bd0c2c81029..8de94aa899db 100644 --- a/solr/webapp/web/js/angular/controllers/cores.js +++ b/solr/webapp/web/js/angular/controllers/cores.js @@ -173,7 +173,7 @@ solrAdminApp.controller('CoreAdminController', $scope.reloadCore = function() { if ($scope.initFailures[$scope.selectedCore]) { delete $scope.initFailures[$scope.selectedCore]; - $scope.showInitFailures = Object.keys(data.initFailures).length>0; + $scope.showInitFailures = Object.keys($scope.initFailures).length>0; } CoresV2.reloadCore($scope.selectedCore, function(error, data, response) { diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 9d4f3bd980b0..7741916a2d03 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -147,10 +147,6 @@ solrAdminServices.factory('System', // $resource, like SchemaDesigner/Security/Segments. return $resource('/api/node/threads', {'wt':'json', '_':Date.now()}); }]) -.factory('Properties', - ['$resource', function($resource) { - return $resource('admin/info/properties', {'wt':'json', '_':Date.now()}); - }]) .factory('Replication', ['$resource', function($resource) { return $resource(':core/replication', {'wt':'json', core: "@core", '_':Date.now()}, { From a8cd6cc454d0592b2bf83193da8e001a5904adb8 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 24 Jul 2026 13:44:28 -0300 Subject: [PATCH 10/20] More V2 API us for list collections and aliasss. --- solr/webapp/web/js/angular/app.js | 68 ++++++++++++++------------ solr/webapp/web/js/angular/services.js | 18 +++---- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index f7143678d7bb..b47865816d52 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -495,7 +495,7 @@ solrAdminApp.config([ }; }); -solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, Collections, SystemV2, Ping, Constants, SchemaDesigner) { +solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, CollectionsV2, AliasesV2, SystemV2, Ping, Constants, SchemaDesigner) { $rootScope.exceptions={}; @@ -554,37 +554,43 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ var currentCollectionName = $route.current.params.core; delete $scope.currentCollection; if ($scope.isCloudEnabled) { - Collections.list(function (cdata) { - Collections.listaliases(function (adata) { - $scope.aliases = []; - for (var key in adata.aliases) { - props = {}; - if (key in adata.properties) { - props = adata.properties[key]; - } - var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; - $scope.aliases.push(alias); - if (pageType == Constants.IS_COLLECTION_PAGE && alias.name == currentCollectionName) { - $scope.currentCollection = alias; - } - } - $scope.collections = []; - for (key in cdata.collections) { - if (cdata.collections[key].startsWith("._designer_")) { - continue; // ignore temp designer collections - } - var collection = {name: cdata.collections[key], type: 'collection'}; - $scope.collections.push(collection); - if (pageType == Constants.IS_COLLECTION_PAGE && collection.name == currentCollectionName) { - $scope.currentCollection = collection; - } - } + CollectionsV2.listCollections(function (error, cdata, response) { + $timeout(function() { + if (error) return; + AliasesV2.getAliases(function (error, adata, response) { + $timeout(function() { + if (error) return; + $scope.aliases = []; + for (var key in adata.aliases) { + props = {}; + if (key in adata.properties) { + props = adata.properties[key]; + } + var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props}; + $scope.aliases.push(alias); + if (pageType == Constants.IS_COLLECTION_PAGE && alias.name == currentCollectionName) { + $scope.currentCollection = alias; + } + } + $scope.collections = []; + for (key in cdata.collections) { + if (cdata.collections[key].startsWith("._designer_")) { + continue; // ignore temp designer collections + } + var collection = {name: cdata.collections[key], type: 'collection'}; + $scope.collections.push(collection); + if (pageType == Constants.IS_COLLECTION_PAGE && collection.name == currentCollectionName) { + $scope.currentCollection = collection; + } + } - $scope.aliases_and_collections = $scope.aliases; - if ($scope.aliases.length > 0) { - $scope.aliases_and_collections = $scope.aliases_and_collections.concat({name:'-----'}); - } - $scope.aliases_and_collections = $scope.aliases_and_collections.concat($scope.collections); + $scope.aliases_and_collections = $scope.aliases; + if ($scope.aliases.length > 0) { + $scope.aliases_and_collections = $scope.aliases_and_collections.concat({name:'-----'}); + } + $scope.aliases_and_collections = $scope.aliases_and_collections.concat($scope.collections); + }); + }); }); }); } diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 7741916a2d03..6211409c4001 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -99,16 +99,14 @@ solrAdminServices.factory('System', return new solrApi.ClusterApi(); }) .factory('Collections', - ['$resource', function($resource) { - // This v1 factory only covers "list"/"listaliases" (used by app.js's nav-list population) and - // "status"/CLUSTERSTATUS (used by collections.js, cloud.js, collection-overview.js) -- no - // single-request v2 equivalent for CLUSTERSTATUS exists yet, and no other actions here have - // any remaining callers. - return $resource('admin/collections', - {'wt':'json', '_':Date.now()}, { - "list": {params:{action: "LIST"}}, - "listaliases": {params:{action: "LISTALIASES"}}, - "status": {params:{action: "CLUSTERSTATUS"}} + ['$resource', function ($resource) { + // ERIC: NOT SURE ABOUT THIS CHUNK... + // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler + // that v1's CLUSTERSTATUS action used, so the response shape is byte-identical -- no + // generated solrApi client class exists for it (old-style @EndPoint API, predates the + // OpenAPI-based v2 framework), so this stays a plain $resource, like Segments/Threads/ParamSet. + return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, { + "status": {} }); }]) .factory('Logging', From 88f14b9599422dd989ff35362c96b3fd137406c5 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 29 Jul 2026 11:37:30 -0500 Subject: [PATCH 11/20] Finish using SystemV2 throughout now. --- .../web/js/angular/controllers/cloud.js | 93 ++++++++++--------- solr/webapp/web/js/angular/services.js | 6 +- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js index 454c66c43a5b..394ee80a2139 100644 --- a/solr/webapp/web/js/angular/controllers/cloud.js +++ b/solr/webapp/web/js/angular/controllers/cloud.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('CloudController', - function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, MetricsExtractor, ZookeeperStatus) { + function($scope, $location, $timeout, Zookeeper, Constants, Collections, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus) { $scope.showDebug = false; @@ -37,7 +37,7 @@ solrAdminApp.controller('CloudController', graphSubController($scope, Zookeeper, false); } else if (view === "nodes") { $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE); - nodesSubController($scope, Collections, System, Metrics, MetricsExtractor); + nodesSubController($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor); } else if (view === "zkstatus") { $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE); zkStatusSubController($scope, ZookeeperStatus, false); @@ -107,7 +107,7 @@ function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } -var nodesSubController = function($scope, Collections, System, Metrics, MetricsExtractor) { +var nodesSubController = function($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor) { $scope.pageSize = 10; $scope.showNodes = true; $scope.showTree = false; @@ -352,50 +352,51 @@ var nodesSubController = function($scope, Collections, System, Metrics, MetricsE /* Fetch system info for all selected nodes Pick the data we want to display and add it to the node-centric data structure - - Intentionally still v1 (System, not SystemV2): the v2 NodeSystemInfoApi's "nodes" - multi-node proxying (GetNodeSystemInfo.proxyToNodes, via V2SolrRequestBasedProxy) currently - throws a server-side NullPointerException (GetNodeSystemInfo.java, processTypedProxiedResponse) - instead of returning aggregated per-node data. Move this to SystemV2.getNodeSystemInfo once - that's fixed upstream. */ - System.get({"nodes": liveNodesToShow.join(',')}, function (systemResponse) { - for (var node in systemResponse) { - if (node in nodes) { - var s = systemResponse[node]; - nodes[node]['system'] = s; - var memTotal = s.system.totalPhysicalMemorySize; - var memFree = s.system.freePhysicalMemorySize; - var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100); - nodes[node]['memUsedPct'] = memPercentage; - nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage); - nodes[node]['memTotal'] = bytesToSize(memTotal); - nodes[node]['memFree'] = bytesToSize(memFree); - nodes[node]['memUsed'] = bytesToSize(memTotal - memFree); - - var heapMax = s.jvm.memory.raw.max; - var heapTotal = s.jvm.memory.raw.total; - var heapFree = s.jvm.memory.raw.free; - var heapPercentage = Math.floor((heapTotal - heapFree) / heapMax * 100); - nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree); - nodes[node]['heapUsedPct'] = heapPercentage; - nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage); - nodes[node]['heapMax'] = bytesToSize(heapMax); - nodes[node]['heapTotal'] = bytesToSize(heapTotal); - nodes[node]['heapFree'] = bytesToSize(heapFree); - - var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds - nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime); - nodes[node]['jvmUptimeSec'] = jvmUptime; - - nodes[node]['uptime'] = (s.system.uptime || "unknown").replace(/.*up (.*?,.*?),.*/, "$1"); - nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100; - nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad * 100); - nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad)); - nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount; - nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount; - } + SystemV2.getNodeSystemInfo({"nodes": liveNodesToShow.join(',')}, function (error, data, response) { + if (error) { + console.error('Failed to fetch node system info:', error); + return; } + $timeout(function() { + var systemResponse = response.body; + for (var node in systemResponse) { + if (node in nodes) { + var s = systemResponse[node]; + nodes[node]['system'] = s; + var memTotal = s.system.totalPhysicalMemorySize; + var memFree = s.system.freePhysicalMemorySize; + var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100); + nodes[node]['memUsedPct'] = memPercentage; + nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage); + nodes[node]['memTotal'] = bytesToSize(memTotal); + nodes[node]['memFree'] = bytesToSize(memFree); + nodes[node]['memUsed'] = bytesToSize(memTotal - memFree); + + var heapMax = s.jvm.memory.raw.max; + var heapTotal = s.jvm.memory.raw.total; + var heapFree = s.jvm.memory.raw.free; + var heapPercentage = Math.floor((heapTotal - heapFree) / heapMax * 100); + nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree); + nodes[node]['heapUsedPct'] = heapPercentage; + nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage); + nodes[node]['heapMax'] = bytesToSize(heapMax); + nodes[node]['heapTotal'] = bytesToSize(heapTotal); + nodes[node]['heapFree'] = bytesToSize(heapFree); + + var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds + nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime); + nodes[node]['jvmUptimeSec'] = jvmUptime; + + nodes[node]['uptime'] = (s.system.uptime || "unknown").replace(/.*up (.*?,.*?),.*/, "$1"); + nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100; + nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad * 100); + nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad)); + nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount; + nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount; + } + } + }); }); /* @@ -798,7 +799,7 @@ var graphSubController = function ($scope, Zookeeper) { params.filter = filter; } - Zookeeper.clusterState(params, function (data) { + Zookeeper.clusterState(params, function (data) { var state = data.znode.data; var leaf_count = 0; var graph_data = { diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 6211409c4001..e238354c0d50 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -17,11 +17,7 @@ var solrAdminServices = angular.module('solrAdminServices', ['ngResource']); -solrAdminServices.factory('System', - ['$resource', function($resource) { - return $resource('admin/info/system', {"wt":"json", "nodes": "@nodes", "_":Date.now()}); - }]) -.factory('Metrics', +solrAdminServices.factory('Metrics', ['$resource', 'PrometheusParser', function($resource, PrometheusParser) { return $resource('admin/metrics', {"wt":"prometheus", "node": "@node", "_":Date.now()}, { get: { From 68c7aaee63534202ebb86a1360f6d1968df7a3ce Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 29 Jul 2026 11:59:58 -0500 Subject: [PATCH 12/20] Migrate Segments to V2 api. --- .../web/js/angular/controllers/segments.js | 50 +++++++++++-------- solr/webapp/web/js/angular/services.js | 19 ++++--- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/segments.js b/solr/webapp/web/js/angular/controllers/segments.js index e835cc08496e..51c61e772224 100644 --- a/solr/webapp/web/js/angular/controllers/segments.js +++ b/solr/webapp/web/js/angular/controllers/segments.js @@ -17,41 +17,47 @@ var MB_FACTOR = 1024*1024; -solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $interval, Segments, Constants) { +solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $interval, $timeout, SegmentsV2, Constants) { $scope.resetMenu("segments", Constants.IS_CORE_PAGE); $scope.refresh = function() { - Segments.get({core: $routeParams.core}, function(data) { - var segments = data.segments; + SegmentsV2.getSegmentData($routeParams.core, {}, function(error, data, response) { + if (error) { + console.error('Failed to fetch segment data:', error); + return; + } + $timeout(function() { + var segments = data.segments; - var segmentSizeInBytesMax = getLargestSegmentSize(segments); - $scope.segmentMB = Math.floor(segmentSizeInBytesMax / MB_FACTOR); - $scope.xaxis = calculateXAxis(segmentSizeInBytesMax); + var segmentSizeInBytesMax = getLargestSegmentSize(segments); + $scope.segmentMB = Math.floor(segmentSizeInBytesMax / MB_FACTOR); + $scope.xaxis = calculateXAxis(segmentSizeInBytesMax); - $scope.documentCount = 0; - $scope.deletionCount = 0; + $scope.documentCount = 0; + $scope.deletionCount = 0; - $scope.segments = []; - for (var name in segments) { - var segment = segments[name]; + $scope.segments = []; + for (var name in segments) { + var segment = segments[name]; - var segmentSizeInBytesLog = Math.log(segment.sizeInBytes); - var segmentSizeInBytesMaxLog = Math.log(segmentSizeInBytesMax); + var segmentSizeInBytesLog = Math.log(segment.sizeInBytes); + var segmentSizeInBytesMaxLog = Math.log(segmentSizeInBytesMax); - segment.totalSize = Math.floor((segmentSizeInBytesLog / segmentSizeInBytesMaxLog ) * 100); + segment.totalSize = Math.floor((segmentSizeInBytesLog / segmentSizeInBytesMaxLog ) * 100); - segment.deletedDocSize = Math.floor((segment.delCount / segment.size) * segment.totalSize); - if (segment.delDocSize <= 0.001) delete segment.deletedDocSize; + segment.deletedDocSize = Math.floor((segment.delCount / segment.size) * segment.totalSize); + if (segment.delDocSize <= 0.001) delete segment.deletedDocSize; - segment.aliveDocSize = segment.totalSize - segment.deletedDocSize; + segment.aliveDocSize = segment.totalSize - segment.deletedDocSize; - $scope.segments.push(segment); + $scope.segments.push(segment); - $scope.documentCount += segment.size; - $scope.deletionCount += segment.delCount; - } - $scope.deletionsPercentage = calculateDeletionsPercentage($scope.documentCount, $scope.deletionCount); + $scope.documentCount += segment.size; + $scope.deletionCount += segment.delCount; + } + $scope.deletionsPercentage = calculateDeletionsPercentage($scope.documentCount, $scope.deletionCount); + }); }); }; diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index e238354c0d50..90d4bee19082 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -94,13 +94,19 @@ solrAdminServices.factory('Metrics', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.ClusterApi(); }) +.factory('SegmentsV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.SegmentsApi(); + }) .factory('Collections', ['$resource', function ($resource) { // ERIC: NOT SURE ABOUT THIS CHUNK... // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler // that v1's CLUSTERSTATUS action used, so the response shape is byte-identical -- no // generated solrApi client class exists for it (old-style @EndPoint API, predates the - // OpenAPI-based v2 framework), so this stays a plain $resource, like Segments/Threads/ParamSet. + // OpenAPI-based v2 framework), so this stays a plain $resource, like Threads/ParamSet. return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, { "status": {} }); @@ -138,7 +144,7 @@ solrAdminServices.factory('Metrics', // v2 NodeThreadsAPI (/api/node/threads) still just delegates straight through to the same v1 // ThreadDumpHandler, so the response shape is byte-identical -- no generated solrApi client // class exists for it (it predates the OpenAPI-based v2 API framework), so this stays a plain - // $resource, like SchemaDesigner/Security/Segments. + // $resource, like SchemaDesigner/Security. return $resource('/api/node/threads', {'wt':'json', '_':Date.now()}); }]) .factory('Replication', @@ -167,8 +173,7 @@ solrAdminServices.factory('Metrics', // v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still // delegate straight through to the same v1 SolrConfigHandler, so the response shape is // byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API, - // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like - // Segments/Threads. + // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like Threads. // NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a // collection name (SolrCloud) or an actual core name (standalone/user-managed) -- // /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK), @@ -279,12 +284,6 @@ solrAdminServices.factory('Metrics', } return resource; }]) -.factory('Segments', - ['$resource', function($resource) { - return $resource('/api/cores/:core/segments', {'wt':'json', core: '@core', _:Date.now()}, { - get: {} - }); -}]) .factory('Schema', ['$resource', function($resource) { return $resource(':core/schema', {wt: 'json', core: '@core', _:Date.now()}, { From b50670b7848602b489fdad361b56ce317d10977a Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 29 Jul 2026 18:54:45 -0400 Subject: [PATCH 13/20] Fix reloadCore success/failure indicator not rendering The V2 client callback runs outside Angular's digest cycle, so setting $scope.reloadSuccess/reloadFailure directly (with only the delayed reset wrapped in $timeout) never triggered a re-render -- the ng-class success/warn flash on the Reload button silently never appeared. Wrap the whole callback body in $timeout instead. Co-Authored-By: Claude Sonnet 5 --- .../web/js/angular/controllers/cores.js | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/cores.js b/solr/webapp/web/js/angular/controllers/cores.js index 8de94aa899db..30c92e74cc01 100644 --- a/solr/webapp/web/js/angular/controllers/cores.js +++ b/solr/webapp/web/js/angular/controllers/cores.js @@ -177,19 +177,21 @@ solrAdminApp.controller('CoreAdminController', } CoresV2.reloadCore($scope.selectedCore, function(error, data, response) { - if (error) { - $scope.reloadFailure = true; - $timeout(function() { - $scope.reloadFailure = false; - $route.reload(); - }, 1000); - } else { - $scope.reloadSuccess = true; - $timeout(function () { - $scope.reloadSuccess = false; - $route.reload(); - }, 1000); - } + $timeout(function() { + if (error) { + $scope.reloadFailure = true; + $timeout(function() { + $scope.reloadFailure = false; + $route.reload(); + }, 1000); + } else { + $scope.reloadSuccess = true; + $timeout(function () { + $scope.reloadSuccess = false; + $route.reload(); + }, 1000); + } + }); }); }; From 7bbdcbb1aa1de164e33d4f7e846fe283a286b1ee Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 29 Jul 2026 18:54:53 -0400 Subject: [PATCH 14/20] Fix collections.js UI feedback flags and reloadCollection callback Same digest-cycle issue as cores.js's reloadCore: deleteShard, deleteReplica, addReplica, and reloadCollection set their success/failure/deleted flags outside $timeout, so the ng-class indicators never rendered. Wrap each callback body in $timeout. Also found and fixed a separate bug in reloadCollection while verifying the above: CollectionsApi.reloadCollection requires (collectionName, opts, callback), but the call only passed (collectionName, callback) -- the callback function was landing in the opts slot, so the generated client's `if (callback)` guard silently no-op'd and neither branch ever ran. Confirmed via browser testing that reload requests fired but had zero UI effect until this was fixed. Co-Authored-By: Claude Sonnet 5 --- .../web/js/angular/controllers/collections.js | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js index 9e4a23026270..6aceb7399157 100644 --- a/solr/webapp/web/js/angular/controllers/collections.js +++ b/solr/webapp/web/js/angular/controllers/collections.js @@ -232,15 +232,17 @@ solrAdminApp.controller('CollectionsController', alert("No collection selected."); return; } - CollectionsV2.reloadCollection($scope.collection.name, function(error, data,response) { - if (error) { - $scope.reloadFailure = true; - $timeout(function() {$scope.reloadFailure=false}, 1000); - $location.path("/~collections"); - } else { - $scope.reloadSuccess = true; - $timeout(function() {$scope.reloadSuccess=false}, 1000); - } + CollectionsV2.reloadCollection($scope.collection.name, {}, function(error, data,response) { + $timeout(function() { + if (error) { + $scope.reloadFailure = true; + $timeout(function() {$scope.reloadFailure=false}, 1000); + $location.path("/~collections"); + } else { + $scope.reloadSuccess = true; + $timeout(function() {$scope.reloadSuccess=false}, 1000); + } + }); }); }; @@ -269,21 +271,25 @@ solrAdminApp.controller('CollectionsController', $scope.deleteShard = function(shard) { ShardsV2.deleteShard(shard.collection, shard.name, {}, function(error, data, response) { - if (error) return; - shard.deleted = true; $timeout(function() { - $scope.refresh(); - }, 2000); + if (error) return; + shard.deleted = true; + $timeout(function() { + $scope.refresh(); + }, 2000); + }); }); } $scope.deleteReplica = function(replica) { ReplicasV2.deleteReplicaByName(replica.collection, replica.shard, replica.name, {}, function(error, data, response) { - if (error) return; - replica.deleted = true; $timeout(function() { - $scope.refresh(); - }, 2000); + if (error) return; + replica.deleted = true; + $timeout(function() { + $scope.refresh(); + }, 2000); + }); }); } $scope.addReplica = function(shard) { @@ -294,13 +300,15 @@ solrAdminApp.controller('CollectionsController', createReplicaParams.node = shard.replicaNodeName; } ReplicasV2.createReplica(shard.collection, shard.name, {createReplicaRequestBody: createReplicaParams}, function(error, data, response) { - if (error) return; - shard.replicaAdded = true; - $timeout(function () { - shard.replicaAdded = false; - shard.showAdd = false; - $scope.refresh(); - }, 2000); + $timeout(function() { + if (error) return; + shard.replicaAdded = true; + $timeout(function () { + shard.replicaAdded = false; + shard.showAdd = false; + $scope.refresh(); + }, 2000); + }); }); }; From fa2da9b81accb10ee0ef0c430d5f3effff592f66 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Aug 2026 14:25:49 -0400 Subject: [PATCH 15/20] Bug fix handling paramsets (race condition) --- .../web/js/angular/controllers/paramsets.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/paramsets.js b/solr/webapp/web/js/angular/controllers/paramsets.js index 79ce7c53312c..d466c31a8a64 100644 --- a/solr/webapp/web/js/angular/controllers/paramsets.js +++ b/solr/webapp/web/js/angular/controllers/paramsets.js @@ -104,11 +104,18 @@ solrAdminApp.controller('ParamSetsController', } } - $scope.getParamsets(); - if ($routeParams.paramset){ - $scope.name = $routeParams.paramset; - $scope.getParamset($routeParams.paramset); - } + // resetMenu() above populates $scope.isCloudEnabled asynchronously, so calling + // getParamsets() synchronously here would race it and fall back to indexType "cores" + // even in SolrCloud -- wait for isCloudEnabled to settle before the first fetch. + var unwatchCloudEnabled = $scope.$watch('isCloudEnabled', function(value) { + if (value === undefined) return; + unwatchCloudEnabled(); + $scope.getParamsets(); + if ($routeParams.paramset){ + $scope.name = $routeParams.paramset; + $scope.getParamset($routeParams.paramset); + } + }); $scope.refresh = function () { $scope.paramsetContent = ""; From 38e6f3a249e88dd976a9cc51f181e860deb73081 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Aug 2026 14:25:58 -0400 Subject: [PATCH 16/20] using playwright mcp for testing --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9b9fe7a32c81..718b647da464 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ GEMINI.md # WANT TO ADD MORE? You can tell Git without adding to this file: # See https://git-scm.com/docs/gitignore # In particular, if you have tools you use, add to $GIT_DIR/info/exclude or use core.excludesFile +/.playwright-mcp From 4e754496f5d26904d70fa88720e8e1d36f4a8a4e Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Aug 2026 15:41:37 -0400 Subject: [PATCH 17/20] similar to paramsets, the race condition on picking cloud or not... --- solr/webapp/web/js/angular/controllers/query.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/solr/webapp/web/js/angular/controllers/query.js b/solr/webapp/web/js/angular/controllers/query.js index 5c8532cf1a74..1d135e2069c6 100644 --- a/solr/webapp/web/js/angular/controllers/query.js +++ b/solr/webapp/web/js/angular/controllers/query.js @@ -30,7 +30,14 @@ solrAdminApp.controller('QueryController', $scope.val['indent'] = true; $scope.useParams = []; - getParamsets(); + // isCloudEnabled populates asynchronously via resetMenu(), so calling getParamsets() + // synchronously here would race it and fall back to indexType "cores" even in SolrCloud -- + // wait for isCloudEnabled to settle before the first fetch (see paramsets.js for the same fix). + var unwatchCloudEnabled = $scope.$watch('isCloudEnabled', function(value) { + if (value === undefined) return; + unwatchCloudEnabled(); + getParamsets(); + }); function getParamsets() { From f2ea6435acf9c8b9206459dbb1a970bfef899e90 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Aug 2026 15:42:07 -0400 Subject: [PATCH 18/20] Make v2 superagent based apis and v1 $http angular both emit errors in the same way that v1 used to.. --- solr/webapp/web/js/angular/app.js | 10 ++-- .../web/js/angular/controllers/cloud.js | 7 +-- .../web/js/angular/controllers/collections.js | 24 +++++----- .../web/js/angular/controllers/cores.js | 6 +-- .../web/js/angular/controllers/index.js | 4 +- .../web/js/angular/controllers/logging.js | 8 ++-- .../web/js/angular/controllers/security.js | 4 +- .../web/js/angular/controllers/segments.js | 3 +- solr/webapp/web/js/angular/services.js | 46 +++++++++++++++++++ 9 files changed, 81 insertions(+), 31 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index b47865816d52..4f2d67a96bb0 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -495,7 +495,7 @@ solrAdminApp.config([ }; }); -solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, CollectionsV2, AliasesV2, SystemV2, Ping, Constants, SchemaDesigner) { +solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, $timeout, CoresV2, CollectionsV2, AliasesV2, SystemV2, Ping, Constants, SchemaDesigner, ApiErrorHandler) { $rootScope.exceptions={}; @@ -518,7 +518,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ $scope.resetMenu = function(page, pageType) { CoresV2.getAllCoreStatus({indexInfo: false}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.cores = []; var currentCoreName = $route.current.params.core; delete $scope.currentCore; @@ -539,7 +539,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ SystemV2.getNodeSystemInfo({}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.isCloudEnabled = data.mode.match( /solrcloud/i ); $scope.usersPermissions = data.security.permissions; $scope.isSecurityEnabled = $scope.authenticationPlugin != null; @@ -556,10 +556,10 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ if ($scope.isCloudEnabled) { CollectionsV2.listCollections(function (error, cdata, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } AliasesV2.getAliases(function (error, adata, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.aliases = []; for (var key in adata.aliases) { props = {}; diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js index 394ee80a2139..e5b2b1483e8f 100644 --- a/solr/webapp/web/js/angular/controllers/cloud.js +++ b/solr/webapp/web/js/angular/controllers/cloud.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('CloudController', - function($scope, $location, $timeout, Zookeeper, Constants, Collections, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus) { + function($scope, $location, $timeout, Zookeeper, Constants, Collections, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) { $scope.showDebug = false; @@ -37,7 +37,7 @@ solrAdminApp.controller('CloudController', graphSubController($scope, Zookeeper, false); } else if (view === "nodes") { $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE); - nodesSubController($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor); + nodesSubController($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler); } else if (view === "zkstatus") { $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE); zkStatusSubController($scope, ZookeeperStatus, false); @@ -107,7 +107,7 @@ function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } -var nodesSubController = function($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor) { +var nodesSubController = function($scope, $timeout, Collections, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) { $scope.pageSize = 10; $scope.showNodes = true; $scope.showTree = false; @@ -356,6 +356,7 @@ var nodesSubController = function($scope, $timeout, Collections, SystemV2, Metri SystemV2.getNodeSystemInfo({"nodes": liveNodesToShow.join(',')}, function (error, data, response) { if (error) { console.error('Failed to fetch node system info:', error); + ApiErrorHandler.handle(response); return; } $timeout(function() { diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js index 6aceb7399157..e1ae616d0027 100644 --- a/solr/webapp/web/js/angular/controllers/collections.js +++ b/solr/webapp/web/js/angular/controllers/collections.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('CollectionsController', - function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants){ + function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants, ApiErrorHandler){ $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $scope.refresh = function() { @@ -25,7 +25,7 @@ solrAdminApp.controller('CollectionsController', ClusterV2.listClusterNodes(function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.availableNodeSet = data.nodes; }); }); @@ -64,7 +64,7 @@ solrAdminApp.controller('CollectionsController', // Fetch aliases using getAliases to get properties AliasesV2.getAliases(function (error, adata, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } // TODO: Population of aliases array duplicated in app.js $scope.aliases = []; for (var key in adata.aliases) { @@ -90,7 +90,7 @@ solrAdminApp.controller('CollectionsController', }); ConfigSetsV2.listConfigSet(function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.configs = []; var items = data.configSets; for (var i in items) { @@ -145,7 +145,7 @@ solrAdminApp.controller('CollectionsController', } AliasesV2.createAlias({createAliasRequestBody: {name: $scope.aliasToCreate, collections: collections}}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.cancelCreateAlias(); $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $location.path("/~collections/alias_" + $scope.aliasToCreate); @@ -155,7 +155,7 @@ solrAdminApp.controller('CollectionsController', $scope.deleteAlias = function() { AliasesV2.deleteAlias($scope.collection.name, {}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.hideAll(); $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $location.path("/~collections/"); @@ -191,7 +191,7 @@ solrAdminApp.controller('CollectionsController', } CollectionsV2.createCollection({createCollectionRequestBody: createCollectionParams}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.cancelAddCollection(); $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $location.path("/~collections/" + $scope.newCollection.name); @@ -218,7 +218,7 @@ solrAdminApp.controller('CollectionsController', if ($scope.collection.name == $scope.collectionDeleteConfirm) { CollectionsV2.deleteCollection($scope.collection.name, {}, function (error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $location.path("/~collections"); }); }); @@ -253,7 +253,7 @@ solrAdminApp.controller('CollectionsController', ClusterV2.listClusterNodes(function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.nodes = data.nodes; }); }); @@ -272,7 +272,7 @@ solrAdminApp.controller('CollectionsController', $scope.deleteShard = function(shard) { ShardsV2.deleteShard(shard.collection, shard.name, {}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } shard.deleted = true; $timeout(function() { $scope.refresh(); @@ -284,7 +284,7 @@ solrAdminApp.controller('CollectionsController', $scope.deleteReplica = function(replica) { ReplicasV2.deleteReplicaByName(replica.collection, replica.shard, replica.name, {}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } replica.deleted = true; $timeout(function() { $scope.refresh(); @@ -301,7 +301,7 @@ solrAdminApp.controller('CollectionsController', } ReplicasV2.createReplica(shard.collection, shard.name, {createReplicaRequestBody: createReplicaParams}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } shard.replicaAdded = true; $timeout(function () { shard.replicaAdded = false; diff --git a/solr/webapp/web/js/angular/controllers/cores.js b/solr/webapp/web/js/angular/controllers/cores.js index 30c92e74cc01..6d35263918ba 100644 --- a/solr/webapp/web/js/angular/controllers/cores.js +++ b/solr/webapp/web/js/angular/controllers/cores.js @@ -16,13 +16,13 @@ */ solrAdminApp.controller('CoreAdminController', - function($scope, $routeParams, $location, $timeout, $route, CoresV2, Update, Constants){ + function($scope, $routeParams, $location, $timeout, $route, CoresV2, Update, Constants, ApiErrorHandler){ $scope.resetMenu("cores", Constants.IS_ROOT_PAGE); $scope.selectedCore = $routeParams.corename; // use 'corename' not 'core' to distinguish from /solr/:core/ $scope.refresh = function() { CoresV2.getAllCoreStatus({}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } var coreCount = 0; var cores = data.status; for (_obj in cores) coreCount++; @@ -104,7 +104,7 @@ solrAdminApp.controller('CoreAdminController', if( !answer ) return; CoresV2.unloadCore($scope.selectedCore, {}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $location.path("/~cores"); }); }); diff --git a/solr/webapp/web/js/angular/controllers/index.js b/solr/webapp/web/js/angular/controllers/index.js index 60c62b9931eb..553724f1f4a1 100644 --- a/solr/webapp/web/js/angular/controllers/index.js +++ b/solr/webapp/web/js/angular/controllers/index.js @@ -15,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -solrAdminApp.controller('IndexController', function($scope, $timeout, SystemV2, Constants) { +solrAdminApp.controller('IndexController', function($scope, $timeout, SystemV2, Constants, ApiErrorHandler) { $scope.resetMenu("index", Constants.IS_ROOT_PAGE); $scope.reload = function() { SystemV2.getNodeSystemInfo({}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.system = data; const releaseDate = parse_release_date($scope.system.lucene['solr-impl-version']) $scope.releaseDaysOld = (new Date() - releaseDate)/1000/60/60/24; diff --git a/solr/webapp/web/js/angular/controllers/logging.js b/solr/webapp/web/js/angular/controllers/logging.js index fcd36e9cf9f8..6fe1ef08032b 100644 --- a/solr/webapp/web/js/angular/controllers/logging.js +++ b/solr/webapp/web/js/angular/controllers/logging.js @@ -24,13 +24,13 @@ var format_time_content = function( time, timeZone ) { } solrAdminApp.controller('LoggingController', - function($scope, $timeout, $cookies, LoggingV2, Constants){ + function($scope, $timeout, $cookies, LoggingV2, Constants, ApiErrorHandler){ $scope.resetMenu("logging", Constants.IS_ROOT_PAGE); $scope.timezone = $cookies.logging_timezone || "Local"; $scope.refresh = function() { LoggingV2.fetchLocalLogMessages({since: 0}, function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.since = new Date(); $scope.sinceDisplay = format_time_content($scope.since, "Local"); var events = data.history; @@ -101,7 +101,7 @@ solrAdminApp.controller('LoggingController', ) .controller('LoggingLevelController', - function($scope, $timeout, Logging, LoggingV2) { + function($scope, $timeout, Logging, LoggingV2, ApiErrorHandler) { $scope.resetMenu("logging-levels"); var packageOf = function(logger) { @@ -128,7 +128,7 @@ solrAdminApp.controller('LoggingController', $scope.refresh = function() { LoggingV2.listAllLoggersAndLevels(function(error, data, response) { $timeout(function() { - if (error) return; + if (error) { ApiErrorHandler.handle(response); return; } $scope.logging = makeTree(data.loggers, ""); $scope.watcher = data.watcher; $scope.levels = []; diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js index 7ccfd2e0908b..f6cffce6bee4 100644 --- a/solr/webapp/web/js/angular/controllers/security.js +++ b/solr/webapp/web/js/angular/controllers/security.js @@ -15,7 +15,7 @@ limitations under the License. */ -solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cookies, $window, Constants, SystemV2, Security) { +solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cookies, $window, Constants, SystemV2, Security, ApiErrorHandler) { $scope.resetMenu("security", Constants.IS_ROOT_PAGE); $scope.params = []; @@ -283,6 +283,8 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki $scope.isSecurityAdminEnabled = true; $scope.hasSecurityEditPerm = false; $scope.hideAll(); + } else { + ApiErrorHandler.handle(response); } return; } diff --git a/solr/webapp/web/js/angular/controllers/segments.js b/solr/webapp/web/js/angular/controllers/segments.js index 51c61e772224..267533e1fa56 100644 --- a/solr/webapp/web/js/angular/controllers/segments.js +++ b/solr/webapp/web/js/angular/controllers/segments.js @@ -17,7 +17,7 @@ var MB_FACTOR = 1024*1024; -solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $interval, $timeout, SegmentsV2, Constants) { +solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $interval, $timeout, SegmentsV2, Constants, ApiErrorHandler) { $scope.resetMenu("segments", Constants.IS_CORE_PAGE); $scope.refresh = function() { @@ -25,6 +25,7 @@ solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $in SegmentsV2.getSegmentData($routeParams.core, {}, function(error, data, response) { if (error) { console.error('Failed to fetch segment data:', error); + ApiErrorHandler.handle(response); return; } $timeout(function() { diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 90d4bee19082..dd462f927232 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -40,6 +40,52 @@ solrAdminServices.factory('Metrics', } }); }]) +.factory('ApiErrorHandler', + ['$rootScope', '$location', '$timeout', function($rootScope, $location, $timeout) { + // v2 API calls go through the generated OpenAPI client, which uses superagent directly and + // so never passes through Angular's $http -- meaning httpInterceptor in app.js (the global + // error banner, 401 redirect, 403 handling) never fires for them. This mirrors + // httpInterceptor's responseError handling for v2's superagent responses, so a failure looks + // the same to the user regardless of which API generation served it. Call from any v2 + // callback's error branch: `if (error) { ApiErrorHandler.handle(response); return; }` + function handle(response) { + if (!response) { + return; + } + // superagent callbacks fire outside Angular's digest cycle, so changes here are invisible + // until the next digest -- $timeout both defers and triggers one. + $timeout(function() { + if (response.status === 401) { + var headers = response.headers || {}; + sessionStorage.setItem("auth.wwwAuthHeader", headers['www-authenticate']); + sessionStorage.setItem("auth.authDataHeader", headers['x-solr-authdata']); + sessionStorage.setItem("auth.statusText", response.statusText); + sessionStorage.setItem("http401", "true"); + sessionStorage.removeItem("auth.scheme"); + sessionStorage.removeItem("auth.realm"); + sessionStorage.removeItem("auth.username"); + sessionStorage.removeItem("auth.header"); + sessionStorage.removeItem("auth.state"); + if ($location.path().includes('/login')) { + if (!sessionStorage.getItem("auth.location")) { + sessionStorage.setItem("auth.location", "/"); + } + } else { + sessionStorage.setItem("auth.location", $location.path()); + $location.path('/login'); + } + } else if (response.status === 403) { + $rootScope.showAuthzFailures = true; + } else { + var url = (response.req && response.req.url) || (response.status + ' ' + $location.url()); + var body = response.body || {}; + var msg = (body.error && body.error.msg) || response.statusText || "Unknown error"; + $rootScope.exceptions[url] = {msg: msg}; + } + }); + } + return {handle: handle}; + }]) .factory('CollectionsV2', function() { solrApi.ApiClient.instance.basePath = '/api'; From e10a81e5d05ee9c989e25d6fc5e1b0d42d762635 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 4 Aug 2026 20:56:58 -0400 Subject: [PATCH 19/20] copilot fixes... --- solr/webapp/web/js/angular/app.js | 8 ++++---- solr/webapp/web/js/angular/controllers/cores.js | 8 ++++++-- solr/webapp/web/js/angular/controllers/logging.js | 2 +- solr/webapp/web/js/angular/controllers/segments.js | 3 +-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index 4f2d67a96bb0..b5467a801139 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -522,7 +522,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ $scope.cores = []; var currentCoreName = $route.current.params.core; delete $scope.currentCore; - for (key in data.status) { + for (var key in data.status) { var core = data.status[key]; if (core.name.startsWith("._designer_")) { continue; @@ -542,7 +542,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ if (error) { ApiErrorHandler.handle(response); return; } $scope.isCloudEnabled = data.mode.match( /solrcloud/i ); $scope.usersPermissions = data.security.permissions; - $scope.isSecurityEnabled = $scope.authenticationPlugin != null; + $scope.isSecurityEnabled = data.security.authenticationPlugin != null; $scope.isSchemaDesignerEnabled = $scope.isPermitted([ permissions.CONFIG_EDIT_PERM, @@ -562,7 +562,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ if (error) { ApiErrorHandler.handle(response); return; } $scope.aliases = []; for (var key in adata.aliases) { - props = {}; + var props = {}; if (key in adata.properties) { props = adata.properties[key]; } @@ -573,7 +573,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ } } $scope.collections = []; - for (key in cdata.collections) { + for (var key in cdata.collections) { if (cdata.collections[key].startsWith("._designer_")) { continue; // ignore temp designer collections } diff --git a/solr/webapp/web/js/angular/controllers/cores.js b/solr/webapp/web/js/angular/controllers/cores.js index 6d35263918ba..c6c6b311488f 100644 --- a/solr/webapp/web/js/angular/controllers/cores.js +++ b/solr/webapp/web/js/angular/controllers/cores.js @@ -25,12 +25,13 @@ solrAdminApp.controller('CoreAdminController', if (error) { ApiErrorHandler.handle(response); return; } var coreCount = 0; var cores = data.status; - for (_obj in cores) coreCount++; + for (var _obj in cores) coreCount++; $scope.hasCores = coreCount >0; if (!$scope.selectedCore && coreCount==0) { $scope.showAddCore(); return; } else if (!$scope.selectedCore) { + var firstCore; for (firstCore in cores) break; $scope.selectedCore = firstCore; $location.path("/~cores/" + $scope.selectedCore).replace(); @@ -68,7 +69,7 @@ solrAdminApp.controller('CoreAdminController', if (!$scope.newCore.name) { $scope.addMessage = "Please provide a core name"; } else if (false) { //@todo detect whether core exists - $scope.AddMessage = "A core with that name already exists"; + $scope.addMessage = "A core with that name already exists"; } else { var createCoreParams = { name: $scope.newCore.name, @@ -84,6 +85,7 @@ solrAdminApp.controller('CoreAdminController', CoresV2.createCore({createCoreParams: createCoreParams}, function(error, data, response) { $timeout(function() { if (error) { + ApiErrorHandler.handle(response); $scope.addMessage = "Error creating core: " + error; return; } @@ -124,6 +126,7 @@ solrAdminApp.controller('CoreAdminController', CoresV2.renameCore($scope.selectedCore, {renameCoreRequestBody: {to: $scope.other}}, function(error, data, response) { $timeout(function() { if (error) { + ApiErrorHandler.handle(response); $scope.renameMessage = "Error renaming core: " + error; return; } @@ -154,6 +157,7 @@ solrAdminApp.controller('CoreAdminController', CoresV2.swapCores($scope.selectedCore, {swapCoresRequestBody: {with: $scope.swapOther}}, function(error, data, response) { $timeout(function() { if (error) { + ApiErrorHandler.handle(response); $scope.swapMessage = "Error swapping cores: " + error; return; } diff --git a/solr/webapp/web/js/angular/controllers/logging.js b/solr/webapp/web/js/angular/controllers/logging.js index 6fe1ef08032b..2011bf9f7d56 100644 --- a/solr/webapp/web/js/angular/controllers/logging.js +++ b/solr/webapp/web/js/angular/controllers/logging.js @@ -132,7 +132,7 @@ solrAdminApp.controller('LoggingController', $scope.logging = makeTree(data.loggers, ""); $scope.watcher = data.watcher; $scope.levels = []; - for (level in data.levels) { + for (var level in data.levels) { $scope.levels.push({name:data.levels[level], pos:level}); } }); diff --git a/solr/webapp/web/js/angular/controllers/segments.js b/solr/webapp/web/js/angular/controllers/segments.js index 267533e1fa56..7043bae5a984 100644 --- a/solr/webapp/web/js/angular/controllers/segments.js +++ b/solr/webapp/web/js/angular/controllers/segments.js @@ -48,9 +48,8 @@ solrAdminApp.controller('SegmentsController', function($scope, $routeParams, $in segment.totalSize = Math.floor((segmentSizeInBytesLog / segmentSizeInBytesMaxLog ) * 100); segment.deletedDocSize = Math.floor((segment.delCount / segment.size) * segment.totalSize); - if (segment.delDocSize <= 0.001) delete segment.deletedDocSize; - segment.aliveDocSize = segment.totalSize - segment.deletedDocSize; + if (segment.deletedDocSize <= 0.001) delete segment.deletedDocSize; $scope.segments.push(segment); From 9ce63e5bbb3a4fbc49107ff5ae1da6cf12e0d07f Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 6 Aug 2026 10:29:52 -0400 Subject: [PATCH 20/20] claude and copilot sitting in a tree, arguing with each other ;-). --- solr/webapp/web/js/angular/controllers/collections.js | 2 +- solr/webapp/web/js/angular/services.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js index e1ae616d0027..93fd5fb1b6d1 100644 --- a/solr/webapp/web/js/angular/controllers/collections.js +++ b/solr/webapp/web/js/angular/controllers/collections.js @@ -68,7 +68,7 @@ solrAdminApp.controller('CollectionsController', // TODO: Population of aliases array duplicated in app.js $scope.aliases = []; for (var key in adata.aliases) { - props = {}; + var props = {}; if (key in adata.properties) { props = adata.properties[key]; } diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index dd462f927232..89da950681d5 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -80,6 +80,8 @@ solrAdminServices.factory('Metrics', var url = (response.req && response.req.url) || (response.status + ' ' + $location.url()); var body = response.body || {}; var msg = (body.error && body.error.msg) || response.statusText || "Unknown error"; + // MainController normally sets this up first, but don't assume that ordering here. + $rootScope.exceptions = $rootScope.exceptions || {}; $rootScope.exceptions[url] = {msg: msg}; } });