You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2022/07/08 14:09:05 UTC

[solr] branch main updated: SOLR-15853 Solr paramset Admin UI implementation (#923)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 277f9e69449 SOLR-15853 Solr paramset Admin UI implementation  (#923)
277f9e69449 is described below

commit 277f9e69449322ba8079a095f655ef8e7f8b30ff
Author: Eric Pugh <ep...@opensourceconnections.com>
AuthorDate: Fri Jul 8 10:09:00 2022 -0400

    SOLR-15853 Solr paramset Admin UI implementation  (#923)
    
    Co-authored-by: betulince <bt...@gmail.com>
---
 solr/CHANGES.txt                                   |   2 +
 solr/webapp/web/css/angular/menu.css               |   1 +
 solr/webapp/web/css/angular/paramsets.css          | 177 +++++++++++++++++++++
 solr/webapp/web/img/ico/paramset.png               | Bin 0 -> 275 bytes
 solr/webapp/web/index.html                         |   4 +
 solr/webapp/web/js/angular/app.js                  |   4 +
 .../webapp/web/js/angular/controllers/paramsets.js | 158 ++++++++++++++++++
 solr/webapp/web/js/angular/controllers/query.js    |  61 ++++++-
 solr/webapp/web/js/angular/services.js             |   7 +
 solr/webapp/web/libs/angular-chosen.min.js         |  20 ---
 solr/webapp/web/partials/paramsets.html            |  93 +++++++++++
 solr/webapp/web/partials/query.html                |   9 ++
 12 files changed, 511 insertions(+), 25 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d04a5d7fb43..ba8d5bea868 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -35,6 +35,8 @@ New Features
 
 * SOLR-15921: Load jars in <solr-install-dir>/lib/ by default (janhoy)
 
+* SOLR-15853: Admin UI support for managing Paramsets and using in Queries. (Betul Ince, Eric Pugh)
+
 Improvements
 ---------------------
 * SOLR-15986: CommitUpdateCommand and SplitIndexCommand can write user commit metadata. (Bruno Roustant)
diff --git a/solr/webapp/web/css/angular/menu.css b/solr/webapp/web/css/angular/menu.css
index a89e7ca3ec6..47bf5c8458a 100644
--- a/solr/webapp/web/css/angular/menu.css
+++ b/solr/webapp/web/css/angular/menu.css
@@ -289,6 +289,7 @@ limitations under the License.
 .sub-menu .stream a { background-image: url( ../../img/ico/node.png ); }
 .sub-menu .analysis a { background-image: url( ../../img/ico/funnel.png ); }
 .sub-menu .documents a { background-image: url( ../../img/ico/documents-stack.png ); }
+.sub-menu .paramsets a { background-image: url( ../../img/ico/paramset.png ); }
 .sub-menu .files a { background-image: url( ../../img/ico/folder.png ); }
 .sub-menu .schema a { background-image: url( ../../img/ico/book-open-text.png ); }
 .sub-menu .replication a { background-image: url( ../../img/ico/node.png ); }
diff --git a/solr/webapp/web/css/angular/paramsets.css b/solr/webapp/web/css/angular/paramsets.css
new file mode 100644
index 00000000000..704fe2eae61
--- /dev/null
+++ b/solr/webapp/web/css/angular/paramsets.css
@@ -0,0 +1,177 @@
+/*
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+*/
+
+#content #paramsets
+{
+    background-image: url( ../../img/div.gif );
+    background-position: 45% 0;
+    background-repeat: repeat-y;
+}
+
+#content #paramsets #form
+{
+    float: left;
+}
+
+#content #paramsets #sample-paramset
+{
+    margin-top: 15px;
+}
+
+#content #paramsets #delete-paramset
+{
+    margin-top: 5px;
+    margin-bottom: 5px;
+}
+
+
+#content #paramsets #form fieldset legend, #content #paramsets #form .optional.expanded legend {
+    display: block;
+    margin-left: 10px;
+    margin-top: 15px;
+    padding: 0px 5px;
+}
+
+legend {
+    display: block;
+    padding-inline-start: 2px;
+    padding-inline-end: 2px;
+    border-width: initial;
+    border-style: none;
+    border-color: initial;
+    border-image: initial;
+}
+
+#content #paramsets #form label
+{
+    cursor: pointer;
+    display: block;
+    margin-top: 5px;
+}
+
+#content #paramsets #form input,
+#content #paramsets #form select,
+#content #paramsets #form textarea
+{
+    margin-bottom: 2px;
+}
+
+#content #paramsets #form input,
+#content #paramsets #form textarea
+{
+    margin-bottom: 2px;
+}
+
+#content #paramsets #form #start
+{
+    float: left;
+}
+
+#content #paramsets #form #rows
+{
+    float: right;
+}
+
+#content #paramsets #form .checkbox input
+{
+    margin-bottom: 0;
+    width: auto;
+}
+
+#content #paramsets #form fieldset,
+#content #paramsets #form .optional.expanded
+{
+    border: 1px solid #fff;
+    border-top: 1px solid #c0c0c0;
+    margin-bottom: 5px;
+}
+
+#content #paramsets #form fieldset.common
+{
+    margin-top: 10px;
+}
+
+#content #paramsets #form fieldset legend,
+#content #paramsets #form .optional.expanded legend
+{
+    display: block;
+    margin-left: 10px;
+    padding: 0px 5px;
+}
+
+#content #paramsets #form fieldset legend label
+{
+    margin-top: 0;
+}
+
+#content #paramsets #form fieldset .fieldset
+{
+    border-bottom: 1px solid #f0f0f0;
+    margin-bottom: 5px;
+    padding-bottom: 10px;
+}
+
+#content #paramsets #form .optional
+{
+    border: 0;
+}
+
+#content #paramsets #form .optional legend
+{
+    margin-left: 0;
+    padding-left: 0;
+}
+
+#content #paramsets #form .optional.expanded .fieldset
+{
+    display: block;
+}
+
+#content #paramsets #result
+{
+    float: right;
+    width: 54%;
+}
+
+#content #paramsets #result #response
+{
+}
+
+#content #paramsets #result #response pre
+{
+    padding-left: 20px;
+}
+
+.description{
+    font-weight: bold;
+}
+
+#content #paramsets .chosen-container
+{
+  width: 71% !important;
+}
+
+#content #paramsets .chosen-container {
+  margin-left: 6px;
+  width: 100%;
+}
+#content #paramsets .chosen-drop input,
+#content #paramsets .chosen-results {
+  width: 100% !important;
+}
diff --git a/solr/webapp/web/img/ico/paramset.png b/solr/webapp/web/img/ico/paramset.png
new file mode 100644
index 00000000000..7d2f006917c
Binary files /dev/null and b/solr/webapp/web/img/ico/paramset.png differ
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index b9f27580193..afffe67f764 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -40,6 +40,7 @@ limitations under the License.
   <link rel="stylesheet" type="text/css" href="css/angular/menu.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/plugins.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/documents.css?_=${version}">
+  <link rel="stylesheet" type="text/css" href="css/angular/paramsets.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/query.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/stream.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/replication.css?_=${version}">
@@ -85,6 +86,7 @@ limitations under the License.
   <script src="js/angular/controllers/collection-overview.js?_=${version}"></script>
   <script src="js/angular/controllers/analysis.js?_=${version}"></script>
   <script src="js/angular/controllers/documents.js?_=${version}"></script>
+  <script src="js/angular/controllers/paramsets.js?_=${version}"></script>
   <script src="js/angular/controllers/files.js?_=${version}"></script>
   <script src="js/angular/controllers/query.js?_=${version}"></script>
   <script src="js/angular/controllers/stream.js?_=${version}"></script>
@@ -212,6 +214,7 @@ limitations under the License.
                 <li class="overview" ng-show="currentCollection.type === 'alias'" ng-class="{active:page=='alias-overview'}"><a href="#/{{currentCollection.name}}/alias-overview"><span>Overview</span></a></li>
                 <li class="analysis" ng-show="!isMultiDestAlias(currentCollection)" ng-class="{active:page=='analysis'}"><a href="#/{{currentCollection.name}}/analysis"><span>Analysis</span></a></li>
                 <li class="documents" ng-show="!isMultiDestAlias(currentCollection)" ng-class="{active:page=='documents'}"><a href="#/{{currentCollection.name}}/documents"><span>Documents</span></a></li>
+                <li class="paramsets" ng-show="!isMultiDestAlias(currentCollection)" ng-class="{active:page=='paramsets'}"><a href="#/{{currentCollection.name}}/paramsets"><span>Paramsets</span></a></li>
                 <li class="files" ng-show="!isMultiDestAlias(currentCollection)" ng-class="{active:page=='files'}"><a href="#/{{currentCollection.name}}/files"><span>Files</span></a></li>
                 <li class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCollection.name}}/query"><span>Query</span></a></li>
                 <li class="stream" ng-class="{active:page=='stream'}"><a href="#/{{currentCollection.name}}/stream"><span>Stream</span></a></li>
@@ -237,6 +240,7 @@ limitations under the License.
                 <li class="overview" ng-class="{active:page=='overview'}"><a href="#/{{currentCore.name}}/core-overview"><span>Overview</span></a></li>
                 <li ng-hide="isCloudEnabled" class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCore.name}}/analysis"><span>Analysis</span></a></li>
                 <li ng-hide="isCloudEnabled" class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCore.name}}/documents"><span>Documents</span></a></li>
+                <li ng-hide="isCloudEnabled" class="paramsets" ng-class="{active:page=='paramsets'}"><a href="#/{{currentCore.name}}/paramsets"><span>Paramsets</span></a></li>
                 <li ng-hide="isCloudEnabled" class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCore.name}}/files"><span>Files</span></a></li>
                 <li class="ping" ng-class="{active:page=='ping'}"><a ng-click="ping()"><span>Ping</span><small class="qtime" ng-show="showPing"> (<span>{{pingMS}}ms</span>)</small></a></li>
                 <li class="plugins" ng-class="{active:page=='plugins'}"><a href="#/{{currentCore.name}}/plugins"><span>Plugins / Stats</span></a></li>
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index d0de77acbee..a8bd2f5eb0e 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -135,6 +135,10 @@ solrAdminApp.config([
         templateUrl: 'partials/documents.html',
         controller: 'DocumentsController'
       }).
+      when('/:core/paramsets', {
+        templateUrl: 'partials/paramsets.html',
+        controller: 'ParamSetsController'
+      }).
       when('/:core/files', {
         templateUrl: 'partials/files.html',
         controller: 'FilesController'
diff --git a/solr/webapp/web/js/angular/controllers/paramsets.js b/solr/webapp/web/js/angular/controllers/paramsets.js
new file mode 100644
index 00000000000..e18870b3ce3
--- /dev/null
+++ b/solr/webapp/web/js/angular/controllers/paramsets.js
@@ -0,0 +1,158 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+//helper for formatting JSON and others
+
+solrAdminApp.controller('ParamSetsController',
+  function($scope, $location, $routeParams, ParamSet, Constants) {
+
+    $scope.paramsetList = [];
+
+    $scope.resetMenu("paramsets", Constants.IS_COLLECTION_PAGE);
+
+    $scope.sampleAPICommand = {
+      "set": {
+        "myQueries": {
+          "defType": "edismax",
+          "rows": "5",
+          "df": "text_all"
+        }
+      }
+    }
+
+    $scope.selectParamset = function() {
+      $location.search("paramset", $scope.name);
+      $scope.getParamset($scope.name);
+    }
+
+    $scope.getParamset = function (paramset) {
+      $scope.refresh();
+
+      var params = {};
+      params.core = $routeParams.core;
+      params.wt = "json";
+      params.name = paramset;
+
+      ParamSet.get(params, callback, failure);
+
+      ///////
+
+      function callback(success) {
+        $scope.responseStatus = "success";
+        delete success.$promise;
+        delete success.$resolved;
+        $scope.response = JSON.stringify(success, null, '  ');
+        var apiPayload = {
+          "set": success.response.params
+        };
+        // remove json key that is defined as "", it can't be submitted via the API.
+        var paramsetName = Object.keys(apiPayload.set)[0];
+        delete apiPayload.set[paramsetName][""]
+
+        $scope.paramsetContent = JSON.stringify(apiPayload, null, '  ');
+      }
+
+      function failure (failure) {
+        $scope.responseStatus = failure;
+      }
+    }
+
+
+    $scope.getParamsets = function () {
+      $scope.refresh();
+
+      var params = {};
+      params.core = $routeParams.core;
+      params.wt = "json";
+
+      ParamSet.get(params, callback, failure);
+
+      ///////
+
+      function callback(success) {
+        $scope.responseStatus = "success";
+        delete success.$promise;
+        delete success.$resolved;
+        $scope.response = JSON.stringify(success, null, '  ');
+        $scope.paramsetList = success.response.params ? Object.keys(success.response.params) : [];
+      }
+
+      function failure (failure) {
+        $scope.responseStatus = failure;
+      }
+    }
+
+    $scope.getParamsets();
+    if ($routeParams.paramset){
+      $scope.name = $routeParams.paramset;
+      $scope.getParamset($routeParams.paramset);
+    }
+
+    $scope.refresh = function () {
+      $scope.paramsetContent = "";
+      $scope.placeholder = JSON.stringify($scope.sampleAPICommand, null, '  ');
+    };
+    $scope.refresh();
+
+    $scope.submit = function () {
+      var params = {};
+
+      params.core = $routeParams.core;
+      params.wt = "json";
+
+      ParamSet.submit(params, $scope.paramsetContent, callback, failure);
+
+      ///////
+      function callback(success) {
+        $scope.responseStatus = "success";
+        delete success.$promise;
+        delete success.$resolved;
+        $scope.response = JSON.stringify(success, null, '  ');
+        $scope.name = null;
+        $scope.getParamsets();
+      }
+      function failure (failure) {
+        $scope.responseStatus = failure;
+      }
+    }
+
+    $scope.deleteParamset = function () {
+      var params = {};
+
+      params.core = $routeParams.core;
+      params.wt = "json";
+      params.name = $scope.name;
+
+      var apiPayload = {
+        "delete": [$scope.name]
+      };
+
+      ParamSet.submit(params, apiPayload, callback, failure);
+
+      ///////
+      function callback(success) {
+        $scope.responseStatus = "success";
+        delete success.$promise;
+        delete success.$resolved;
+        $scope.response = JSON.stringify(success, null, '  ');
+        $scope.getParamsets();
+      }
+      function failure (failure) {
+        $scope.responseStatus = failure;
+      }
+    }
+
+  });
diff --git a/solr/webapp/web/js/angular/controllers/query.js b/solr/webapp/web/js/angular/controllers/query.js
index 5e36cfb73bd..14678d26572 100644
--- a/solr/webapp/web/js/angular/controllers/query.js
+++ b/solr/webapp/web/js/angular/controllers/query.js
@@ -16,7 +16,7 @@
 */
 
 solrAdminApp.controller('QueryController',
-  function($scope, $route, $routeParams, $location, Query, Constants){
+  function($scope, $route, $routeParams, $location, Query, Constants, ParamSet){
     $scope.resetMenu("query", Constants.IS_COLLECTION_PAGE);
 
     $scope._models = [];
@@ -27,6 +27,31 @@ solrAdminApp.controller('QueryController',
     $scope.val['q.op'] = "OR";
     $scope.val['defType'] = "";
     $scope.val['indent'] = true;
+    $scope.useParams = [];
+
+    getParamsets();
+
+    function getParamsets() {
+
+      var params = {};
+      params.core = $routeParams.core;
+      params.wt = "json";
+
+      ParamSet.get(params, callback, failure);
+
+      ///////
+
+      function callback(success) {
+        $scope.responseStatus = "success";
+        delete success.$promise;
+        delete success.$resolved;
+        $scope.paramsetList = success.response.params ? Object.keys(success.response.params) : [];
+      }
+
+      function failure (failure) {
+        $scope.responseStatus = failure;
+      }
+    }
 
     // get list of ng-models that have a form element
     function setModels(argTagName){
@@ -68,7 +93,12 @@ solrAdminApp.controller('QueryController',
         $scope.val[argKey] = argValue;
       } else if( $scope._models.map(function(field){return field.modelName}).indexOf(argKey) > -1 ) {
         // parameters that will only be used to generate the admin link
-        $scope[argKey] = argValue;
+        if (argKey === 'useParams'){
+          $scope[argKey] = argValue.split(",")
+        }
+        else {
+          $scope[argKey] = argValue;
+        }
       } else {
         insertToRawParams(argKey, argValue);
       }
@@ -135,16 +165,13 @@ solrAdminApp.controller('QueryController',
                   return param.indexOf(argParam) === 0;
               });
       }
-
       copy(params, $scope.val);
-
       purgeParams(params, ["q.alt", "qf", "mm", "pf", "ps", "qs", "tie", "bq", "bf"], $scope.val.defType !== "dismax" && $scope.val.defType !== "edismax");
       purgeParams(params, ["uf", "pf2", "pf3", "ps2", "ps3", "boost", "stopwords", "lowercaseOperators"], $scope.val.defType !== "edismax");
       purgeParams(params, getDependentFields("hl"), $scope.val.hl !== true);
       purgeParams(params, getDependentFields("facet"), $scope.val.facet !== true);
       purgeParams(params, ["spatial", "pt", "sfield", "d"], $scope.val.spatial !== true);
       purgeParams(params, getDependentFields("spellcheck"), $scope.val.spellcheck !== true);
-
       var qt = $scope.qt ? $scope.qt : "/select";
 
       for (var filter in $scope.filters) {
@@ -173,17 +200,41 @@ solrAdminApp.controller('QueryController',
         params.handler = "select";
         set("qt", qt);
       }
+
+      // convert useParams to array to generate nice URL.
+      if (!Array.isArray($scope.useParams)){
+        params.useParams = $scope.useParams.split(",");
+      }
+      else {
+        params.useParams = $scope.useParams;
+      }
+
       // create rest result url
       var url = Query.url(params);
 
+      // convert useParams back to string
+      if (Array.isArray($scope.useParams)){
+        params.useParams = $scope.useParams.join(",");
+      }
+      else {
+        params.useParams = $scope.useParams;
+      }
+
       // create admin page url
       var adminParams = {...params};
       delete adminParams.handler;
       delete adminParams.core
+      if (!Array.isArray(adminParams.useParams)){
+        adminParams.useParams = adminParams.useParams.split(",");
+      }
       if( $scope.qt != null ) {
         adminParams.qt = [$scope.qt];
       }
       if (isPageReload) {
+        if (!Array.isArray(params.useParams)){
+          params.useParams = params.useParams.split(",");
+        }
+
         Query.query(params, function (data) {
           $scope.lang = $scope.val['wt'];
           if (!$scope.lang || $scope.lang === '') {
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index f8b0b0082bc..1c0b702dbe1 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -126,6 +126,13 @@ solrAdminServices.factory('System',
       "postCsv": {headers: {'Content-type': 'application/csv'}, method: "POST", params: {handler: '@handler'}}
     });
   }])
+.factory('ParamSet',
+  ['$resource', function($resource) {
+    return $resource(':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"}
+    });
+  }])
 .service('FileUpload', function ($http) {
     this.upload = function(params, file, success, error){
         var url = "" + params.core + "/" + params.handler + "?";
diff --git a/solr/webapp/web/libs/angular-chosen.min.js b/solr/webapp/web/libs/angular-chosen.min.js
index b7088e2ba9a..0b12c17492b 100644
--- a/solr/webapp/web/libs/angular-chosen.min.js
+++ b/solr/webapp/web/libs/angular-chosen.min.js
@@ -1,23 +1,3 @@
-/*
-The MIT License
-Copyright (c) 2013 Localytics http://www.localytics.com
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
-
 /**
  * angular-chosen-localytics - Angular Chosen directive is an AngularJS Directive that brings the Chosen jQuery in a Angular way
  * @version v1.9.2
diff --git a/solr/webapp/web/partials/paramsets.html b/solr/webapp/web/partials/paramsets.html
new file mode 100644
index 00000000000..1867b35d927
--- /dev/null
+++ b/solr/webapp/web/partials/paramsets.html
@@ -0,0 +1,93 @@
+<!--
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+
+<div id="paramsets" class="clearfix">
+    <div id="form">
+        <form>
+
+          <fieldset>
+              <legend>Select Paramset</legend>
+            <div id="paramset-name-container">
+
+              <label for="paramsetName">
+                  <a rel="help">Paramsets</a>
+              </label>
+              <select id="paramsetName"
+                      ng-model="name"
+                      chosen
+                      data-placeholder="Please select..."
+                      ng-change="selectParamset()"
+                      ng-options="item for item in paramsetList"><option value=""></option></select>
+
+            </div>
+
+            <div id="delete-paramset" ng-show="name">
+              <button id="delete-paramset" class="warn" ng-click="deleteParamset()">Delete Paramset</button>
+            <div>
+          </fieldset>
+
+            <div>
+                <form>
+                    <label>
+                        <input type="checkbox"
+                               ng-model="switch.showSample">
+                        Sample Paramset
+                    </label>
+                </form>
+                <div ng-switch="switch.showSample">
+                    <div id="sample-paramset" ng-switch-when="true">
+                      <pre class="syntax language-json"><code ng-bind-html="placeholder | highlight:'json' | unsafe"></code></pre>
+                    </div>
+                </div>
+
+                <fieldset>
+                    <legend>Update Paramset(s)</legend>
+                    <div class="fieldset" id="paramsetContent-container">
+                        <label for="paramset">
+                            Paramset(s) JSON
+                        </label>
+                        <textarea ng-model="paramsetContent"
+                                  name="paramsetContent"
+                                  id="paramsetContent"
+                                  title="Request Parameters API Payload"
+                                  rows="10"
+                                  cols="65"></textarea>
+                        <p>
+                          <a href="https://solr.apache.org/guide/solr/latest/configuration-guide/request-parameters-api.html" target="_out">syntax help</a>
+                        </p>
+                    </div>
+                </fieldset>
+
+            </div>
+            <button type="submit" ng-click="submit()" id="submit">Submit Updates</button>
+        </form>
+    </div>
+    <div id="result">
+        <div id="response" ng-show="response">
+            <div>
+                <span class="description">Status: </span>{{ responseStatus }}
+            </div>
+            <div>
+                <span class="description">Response:</span>
+                <pre class="syntax language-json"><code ng-bind-html="response | highlight:'json' | unsafe"></code></pre>
+            </div>
+        </div>
+
+    </div>
+</div>
diff --git a/solr/webapp/web/partials/query.html b/solr/webapp/web/partials/query.html
index f0544ca2a70..58575eb8b9f 100644
--- a/solr/webapp/web/partials/query.html
+++ b/solr/webapp/web/partials/query.html
@@ -77,6 +77,15 @@ limitations under the License.
         </label>
         <input type="text" ng-model="val['df']" name="df" id="df" value="" title="Default search field">
 
+        <label for="useParams">paramset(s)</label>
+        <select id="useParams"
+          ng-model="useParams"
+          title="The paramsets to apply to the query."
+          chosen
+          multiple
+          data-placeholder="Select paramset(s)..."
+          ng-options="item for item in paramsetList"><option value=""></option></select>
+
         <label for="wt" title="The writer type (response format).">
           <a rel="help">wt</a>
         </label>