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

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


The following commit(s) were added to refs/heads/main by this push:
     new 01c721fcda5 Improvements to quota tariffs APIs and UI (#9225)
01c721fcda5 is described below

commit 01c721fcda5f70af88750923e98e2359e3640d13
Author: Bernardo De Marco Gonçalves <[email protected]>
AuthorDate: Thu Aug 15 14:16:44 2024 -0300

    Improvements to quota tariffs APIs and UI (#9225)
    
    * reface quotaTariffList process and add listOnlyRemoved parameter
    
    * add unit tests for createQuotaTariffResponse and 
isUserAllowedToSeeActivationRules methods
    
    * update QuotaTariffListCmdTest
    
    * refactor quota tariffs creation
    
    * refactor quota tariffs update
    
    * fix unit test in JsInterpreter
    
    * remove unused import
    
    * refactor quota listing and add quota deletion
    
    * add functionality to create tariff from UI, not working when specifying 
dates
    
    * fix date parsing
    
    * add labels
    
    * fix details view of tariffs
    
    * new update tariff view
    
    * fix filter placeholder
    
    * remove debug html
    
    * add labels
    
    * make value field to be required when updating a tariff
    
    * add labels
    
    * add portuguese labels
    
    * remove unused label
    
    * fix updating tariff when there was no enddate specified
    
    * refactor dates
    
    * refactor dates
    
    * clear code
    
    * update disabled dates in date picker
    
    * clear ListView component
    
    * fix unnecessary updates when the new end date was equal to the exising 
end date
    
    * fix when today was selected to start date
    
    * add keyword to filter
    
    * change usage type response
    
    * add keyword and usagetype filter on UI
    
    * fix disabled end dates in date picker
    
    * modify datepickers to use datetime
    
    * small fixes
    
    * make value an unrequired field on update form
    
    * remove duplicate import
    
    * remove unused css classes
    
    * add UI support for position parameter
    
    * resize input fields to fill all available horizontal space
    
    * remove console.log()
    
    * remove unnecessary fully qualified names
    
    * replace `usagetypeid` property name to `id` on `listUsageTypes` API call
    
    * replace `usagetypeid` property name to `id` on `listUsageTypes` API call
---
 .../org/apache/cloudstack/api/ApiConstants.java    |   6 +
 .../api/command/admin/usage/ListUsageTypesCmd.java |   5 +-
 .../cloudstack/api/response/UsageTypeResponse.java |  14 +-
 .../org/apache/cloudstack/usage/UsageService.java  |   3 -
 .../org/apache/cloudstack/usage/UsageTypes.java    |  50 ++---
 .../cloudstack/quota/dao/QuotaTariffDao.java       |  12 +-
 .../cloudstack/quota/dao/QuotaTariffDaoImpl.java   | 150 +++------------
 plugins/database/quota/pom.xml                     |   5 +
 .../api/command/QuotaTariffCreateCmd.java          |   7 +-
 .../cloudstack/api/command/QuotaTariffListCmd.java |  33 +++-
 .../api/command/QuotaTariffUpdateCmd.java          |   8 +-
 .../api/response/QuotaResponseBuilder.java         |   5 +-
 .../api/response/QuotaResponseBuilderImpl.java     |  24 ++-
 .../api/command/QuotaTariffListCmdTest.java        |  26 ++-
 .../api/command/QuotaTariffUpdateCmdTest.java      |   4 +-
 .../api/response/QuotaResponseBuilderImplTest.java |  78 +++++++-
 .../java/com/cloud/usage/UsageServiceImpl.java     |   7 -
 ui/public/locales/en.json                          |  49 ++++-
 ui/public/locales/pt_BR.json                       |  53 +++++-
 ui/src/components/view/DetailsTab.vue              |  37 +++-
 ui/src/components/view/ListView.vue                |   8 +-
 ui/src/components/view/SearchView.vue              |  44 ++++-
 ui/src/config/router.js                            |   3 +-
 ui/src/config/section/plugin/quota.js              | 101 ++++++++++-
 ui/src/style/objects/form.scss                     |  28 +++
 ui/src/utils/date.js                               | 104 +++++++++++
 ui/src/utils/plugins.js                            |  27 +--
 ui/src/utils/quota.js                              | 124 +++++++++++++
 ui/src/views/AutogenView.vue                       |   9 +-
 ui/src/views/infra/UsageRecords.vue                |   2 +-
 ui/src/views/plugins/quota/CreateQuotaTariff.vue   | 201 +++++++++++++++++++++
 ui/src/views/plugins/quota/EditQuotaTariff.vue     | 188 +++++++++++++++++++
 ui/src/views/plugins/quota/QuotaTariff.vue         |  63 -------
 .../utils/jsinterpreter/JsInterpreter.java         |   6 +
 .../utils/jsinterpreter/JsInterpreterTest.java     |   4 +-
 35 files changed, 1176 insertions(+), 312 deletions(-)

diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java 
b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 7a167a7aeb6..2f0e4f16797 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -696,6 +696,7 @@ public class ApiConstants {
     public static final String TRAFFIC_TYPE_IMPLEMENTOR = 
"traffictypeimplementor";
     public static final String KEYWORD = "keyword";
     public static final String LIST_ALL = "listall";
+    public static final String LIST_ONLY_REMOVED = "listonlyremoved";
     public static final String LIST_SYSTEM_VMS = "listsystemvms";
     public static final String IP_RANGES = "ipranges";
     public static final String IPV6_ROUTING = "ip6routing";
@@ -1141,6 +1142,11 @@ public class ApiConstants {
 
     public static final String NFS_MOUNT_OPTIONS = "nfsmountopts";
 
+    public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota 
tariff's activation rule. It can receive a JS script that results in either " +
+            "a boolean or a numeric value: if it results in a boolean value, 
the tariff value will be applied according to the result; if it results in a 
numeric value, the " +
+            "numeric value will be applied; if the result is neither a boolean 
nor a numeric value, the tariff will not be applied. If the rule is not 
informed, the tariff " +
+            "value will be applied.";
+
     /**
      * This enum specifies IO Drivers, each option controls specific policies 
on I/O.
      * Qemu guests support "threads" and "native" options Since 0.8.8 ; 
"io_uring" is supported Since 6.3.0 (QEMU 5.0).
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/ListUsageTypesCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/ListUsageTypesCmd.java
index 2772743c75a..b993735dba7 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/ListUsageTypesCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/ListUsageTypesCmd.java
@@ -23,6 +23,7 @@ import org.apache.cloudstack.api.APICommand;
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.response.ListResponse;
 import org.apache.cloudstack.api.response.UsageTypeResponse;
+import org.apache.cloudstack.usage.UsageTypes;
 
 import com.cloud.user.Account;
 
@@ -37,8 +38,8 @@ public class ListUsageTypesCmd extends BaseCmd {
 
     @Override
     public void execute() {
-        List<UsageTypeResponse> result = _usageService.listUsageTypes();
-        ListResponse<UsageTypeResponse> response = new 
ListResponse<UsageTypeResponse>();
+        List<UsageTypeResponse> result = UsageTypes.listUsageTypes();
+        ListResponse<UsageTypeResponse> response = new ListResponse<>();
         response.setResponses(result);
         response.setResponseName(getCommandName());
         this.setResponseObject(response);
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/UsageTypeResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/UsageTypeResponse.java
index 83b97f00c15..5beef5ac556 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/response/UsageTypeResponse.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/response/UsageTypeResponse.java
@@ -25,12 +25,16 @@ import com.cloud.serializer.Param;
 
 public class UsageTypeResponse extends BaseResponse {
 
-    @SerializedName("usagetypeid")
-    @Param(description = "usage type")
+    @SerializedName("id")
+    @Param(description = "Usage type ID")
     private Integer usageType;
 
+    @SerializedName(ApiConstants.NAME)
+    @Param(description = "Usage type name")
+    private String name;
+
     @SerializedName(ApiConstants.DESCRIPTION)
-    @Param(description = "description of usage type")
+    @Param(description = "Usage type description")
     private String description;
 
     public String getDescription() {
@@ -49,10 +53,10 @@ public class UsageTypeResponse extends BaseResponse {
         this.usageType = usageType;
     }
 
-    public UsageTypeResponse(Integer usageType, String description) {
+    public UsageTypeResponse(Integer usageType, String name, String 
description) {
         this.usageType = usageType;
+        this.name = name;
         this.description = description;
         setObjectName("usagetype");
     }
-
 }
diff --git a/api/src/main/java/org/apache/cloudstack/usage/UsageService.java 
b/api/src/main/java/org/apache/cloudstack/usage/UsageService.java
index 73962ba5875..00e8b431f8f 100644
--- a/api/src/main/java/org/apache/cloudstack/usage/UsageService.java
+++ b/api/src/main/java/org/apache/cloudstack/usage/UsageService.java
@@ -20,7 +20,6 @@ import com.cloud.utils.Pair;
 import org.apache.cloudstack.api.command.admin.usage.GenerateUsageRecordsCmd;
 import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd;
 import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd;
-import org.apache.cloudstack.api.response.UsageTypeResponse;
 
 import java.util.List;
 import java.util.TimeZone;
@@ -62,6 +61,4 @@ public interface UsageService {
     TimeZone getUsageTimezone();
 
     boolean removeRawUsageRecords(RemoveRawUsageRecordsCmd cmd);
-
-    List<UsageTypeResponse> listUsageTypes();
 }
diff --git a/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java 
b/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java
index 32ae34056ec..5ad360a8026 100644
--- a/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java
+++ b/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java
@@ -51,31 +51,31 @@ public class UsageTypes {
 
     public static List<UsageTypeResponse> listUsageTypes() {
         List<UsageTypeResponse> responseList = new 
ArrayList<UsageTypeResponse>();
-        responseList.add(new UsageTypeResponse(RUNNING_VM, "Running Vm 
Usage"));
-        responseList.add(new UsageTypeResponse(ALLOCATED_VM, "Allocated Vm 
Usage"));
-        responseList.add(new UsageTypeResponse(IP_ADDRESS, "IP Address 
Usage"));
-        responseList.add(new UsageTypeResponse(NETWORK_BYTES_SENT, "Network 
Usage (Bytes Sent)"));
-        responseList.add(new UsageTypeResponse(NETWORK_BYTES_RECEIVED, 
"Network Usage (Bytes Received)"));
-        responseList.add(new UsageTypeResponse(VOLUME, "Volume Usage"));
-        responseList.add(new UsageTypeResponse(TEMPLATE, "Template Usage"));
-        responseList.add(new UsageTypeResponse(ISO, "ISO Usage"));
-        responseList.add(new UsageTypeResponse(SNAPSHOT, "Snapshot Usage"));
-        responseList.add(new UsageTypeResponse(SECURITY_GROUP, "Security Group 
Usage"));
-        responseList.add(new UsageTypeResponse(LOAD_BALANCER_POLICY, "Load 
Balancer Usage"));
-        responseList.add(new UsageTypeResponse(PORT_FORWARDING_RULE, "Port 
Forwarding Usage"));
-        responseList.add(new UsageTypeResponse(NETWORK_OFFERING, "Network 
Offering Usage"));
-        responseList.add(new UsageTypeResponse(VPN_USERS, "VPN users usage"));
-        responseList.add(new UsageTypeResponse(VM_DISK_IO_READ, "VM Disk 
usage(I/O Read)"));
-        responseList.add(new UsageTypeResponse(VM_DISK_IO_WRITE, "VM Disk 
usage(I/O Write)"));
-        responseList.add(new UsageTypeResponse(VM_DISK_BYTES_READ, "VM Disk 
usage(Bytes Read)"));
-        responseList.add(new UsageTypeResponse(VM_DISK_BYTES_WRITE, "VM Disk 
usage(Bytes Write)"));
-        responseList.add(new UsageTypeResponse(VM_SNAPSHOT, "VM Snapshot 
storage usage"));
-        responseList.add(new UsageTypeResponse(VOLUME_SECONDARY, "Volume on 
secondary storage usage"));
-        responseList.add(new UsageTypeResponse(VM_SNAPSHOT_ON_PRIMARY, "VM 
Snapshot on primary storage usage"));
-        responseList.add(new UsageTypeResponse(BACKUP, "Backup storage 
usage"));
-        responseList.add(new UsageTypeResponse(BUCKET, "Bucket storage 
usage"));
-        responseList.add(new UsageTypeResponse(NETWORK, "Network usage"));
-        responseList.add(new UsageTypeResponse(VPC, "VPC usage"));
+        responseList.add(new UsageTypeResponse(RUNNING_VM, "RUNNING_VM", 
"Running Vm Usage"));
+        responseList.add(new UsageTypeResponse(ALLOCATED_VM, "ALLOCATED_VM", 
"Allocated Vm Usage"));
+        responseList.add(new UsageTypeResponse(IP_ADDRESS, "IP_ADDRESS", "IP 
Address Usage"));
+        responseList.add(new UsageTypeResponse(NETWORK_BYTES_SENT, 
"NETWORK_BYTES_SENT", "Network Usage (Bytes Sent)"));
+        responseList.add(new UsageTypeResponse(NETWORK_BYTES_RECEIVED, 
"NETWORK_BYTES_RECEIVED", "Network Usage (Bytes Received)"));
+        responseList.add(new UsageTypeResponse(VOLUME, "VOLUME", "Volume 
Usage"));
+        responseList.add(new UsageTypeResponse(TEMPLATE, "TEMPLATE", "Template 
Usage"));
+        responseList.add(new UsageTypeResponse(ISO, "ISO", "ISO Usage"));
+        responseList.add(new UsageTypeResponse(SNAPSHOT, "SNAPSHOT", "Snapshot 
Usage"));
+        responseList.add(new UsageTypeResponse(SECURITY_GROUP, 
"SECURITY_GROUP", "Security Group Usage"));
+        responseList.add(new UsageTypeResponse(LOAD_BALANCER_POLICY, 
"LOAD_BALANCER_POLICY", "Load Balancer Usage"));
+        responseList.add(new UsageTypeResponse(PORT_FORWARDING_RULE, 
"PORT_FORWARDING_RULE", "Port Forwarding Usage"));
+        responseList.add(new UsageTypeResponse(NETWORK_OFFERING, 
"NETWORK_OFFERING", "Network Offering Usage"));
+        responseList.add(new UsageTypeResponse(VPN_USERS, "VPN_USERS", "VPN 
users usage"));
+        responseList.add(new UsageTypeResponse(VM_DISK_IO_READ, 
"VM_DISK_IO_READ", "VM Disk usage(I/O Read)"));
+        responseList.add(new UsageTypeResponse(VM_DISK_IO_WRITE, 
"VM_DISK_IO_WRITE", "VM Disk usage(I/O Write)"));
+        responseList.add(new UsageTypeResponse(VM_DISK_BYTES_READ, 
"VM_DISK_BYTES_READ", "VM Disk usage(Bytes Read)"));
+        responseList.add(new UsageTypeResponse(VM_DISK_BYTES_WRITE, 
"VM_DISK_BYTES_WRITE", "VM Disk usage(Bytes Write)"));
+        responseList.add(new UsageTypeResponse(VM_SNAPSHOT, "VM_SNAPSHOT", "VM 
Snapshot storage usage"));
+        responseList.add(new UsageTypeResponse(VOLUME_SECONDARY, 
"VOLUME_SECONDARY", "Volume on secondary storage usage"));
+        responseList.add(new UsageTypeResponse(VM_SNAPSHOT_ON_PRIMARY, 
"VM_SNAPSHOT_ON_PRIMARY", "VM Snapshot on primary storage usage"));
+        responseList.add(new UsageTypeResponse(BACKUP, "BACKUP", "Backup 
storage usage"));
+        responseList.add(new UsageTypeResponse(BUCKET, "BUCKET", "Bucket 
storage usage"));
+        responseList.add(new UsageTypeResponse(NETWORK, "NETWORK", "Network 
usage"));
+        responseList.add(new UsageTypeResponse(VPC, "VPC", "VPC usage"));
         return responseList;
     }
 }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDao.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDao.java
index 4f13fb33180..419bb0ad7d2 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDao.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDao.java
@@ -28,17 +28,9 @@ public interface QuotaTariffDao extends 
GenericDao<QuotaTariffVO, Long> {
 
     Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, Date 
endDate, Integer usageType, String name, String uuid, boolean listAll, Long 
startIndex, Long pageSize);
 
-    QuotaTariffVO findByName(String name);
-
-    QuotaTariffVO findTariffPlanByUsageType(int quotaType, Date onOrBefore);
-
-    Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans();
+    Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, Date 
endDate, Integer usageType, String name, String uuid, boolean listAll, boolean 
listOnlyRemoved, Long startIndex, Long pageSize, String keyword);
 
-    Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(final Long 
startIndex, final Long pageSize);
-
-    Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(Date onOrBefore);
-
-    Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(Date onOrBefore, 
Long startIndex, Long pageSize);
+    QuotaTariffVO findByName(String name);
 
     Boolean updateQuotaTariff(QuotaTariffVO plan);
 
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDaoImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDaoImpl.java
index 8cbec8c8598..d36c698f44d 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDaoImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffDaoImpl.java
@@ -16,12 +16,9 @@
 //under the License.
 package org.apache.cloudstack.quota.dao;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
-import org.apache.cloudstack.quota.constant.QuotaTypes;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import org.apache.commons.collections.CollectionUtils;
 import org.springframework.stereotype.Component;
@@ -34,7 +31,6 @@ import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.Transaction;
 import com.cloud.utils.db.TransactionCallback;
 import com.cloud.utils.db.TransactionLegacy;
-import com.cloud.utils.db.TransactionStatus;
 
 @Component
 public class QuotaTariffDaoImpl extends GenericDaoBase<QuotaTariffVO, Long> 
implements QuotaTariffDao {
@@ -45,7 +41,7 @@ public class QuotaTariffDaoImpl extends 
GenericDaoBase<QuotaTariffVO, Long> impl
     public QuotaTariffDaoImpl() {
         super();
         searchUsageType = createSearchBuilder();
-        searchUsageType.and("usage_type", 
searchUsageType.entity().getUsageType(), SearchCriteria.Op.EQ);
+        searchUsageType.and("usageType", 
searchUsageType.entity().getUsageType(), SearchCriteria.Op.EQ);
         searchUsageType.done();
 
         listAllIncludedUsageType = createSearchBuilder();
@@ -54,111 +50,28 @@ public class QuotaTariffDaoImpl extends 
GenericDaoBase<QuotaTariffVO, Long> impl
         listAllIncludedUsageType.done();
     }
 
-    @Override
-    public QuotaTariffVO findTariffPlanByUsageType(final int quotaType, final 
Date effectiveDate) {
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new 
TransactionCallback<QuotaTariffVO>() {
-            @Override
-            public QuotaTariffVO doInTransaction(final TransactionStatus 
status) {
-                List<QuotaTariffVO> result = new ArrayList<>();
-                final Filter filter = new Filter(QuotaTariffVO.class, 
"updatedOn", false, 0L, 1L);
-                final SearchCriteria<QuotaTariffVO> sc = 
listAllIncludedUsageType.create();
-                sc.setParameters("onorbefore", effectiveDate);
-                sc.setParameters("quotatype", quotaType);
-                result = search(sc, filter);
-                if (result != null && !result.isEmpty()) {
-                    return result.get(0);
-                } else {
-                    if (logger.isDebugEnabled()) {
-                        
logger.debug("QuotaTariffDaoImpl::findTariffPlanByUsageType: Missing quota type 
" + quotaType);
-                    }
-                    return null;
-                }
-            }
-        });
-    }
-
-    @Override
-    public Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans() {
-        return listAllTariffPlans(null, null);
-    }
-
-    @Override
-    public Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(final Long 
startIndex, final Long pageSize) {
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new 
TransactionCallback<Pair<List<QuotaTariffVO>, Integer>>() {
-            @Override
-            public Pair<List<QuotaTariffVO>, Integer> doInTransaction(final 
TransactionStatus status) {
-                return searchAndCount(null, new Filter(QuotaTariffVO.class, 
"updatedOn", false, startIndex, pageSize));
-            }
-        });
-    }
-
-
-    private <T> List<T> paginateList(final List<T> list, final Long 
startIndex, final Long pageSize) {
-        if (startIndex == null || pageSize == null) {
-            return list;
-        }
-        if (list.size() < startIndex){
-            return Collections.emptyList();
-        }
-        return list.subList(startIndex.intValue(), (int) Math.min(startIndex + 
pageSize, list.size()));
-    }
-
-    @Override
-    public Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(final Date 
effectiveDate) {
-        return listAllTariffPlans(effectiveDate, null, null);
-    }
-
-    @Override
-    public Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans(final Date 
effectiveDate, final Long startIndex, final Long pageSize) {
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new 
TransactionCallback<Pair<List<QuotaTariffVO>, Integer>>() {
-            @Override
-            public Pair<List<QuotaTariffVO>, Integer> doInTransaction(final 
TransactionStatus status) {
-                List<QuotaTariffVO> tariffs = new ArrayList<QuotaTariffVO>();
-                final Filter filter = new Filter(QuotaTariffVO.class, 
"updatedOn", false, 0L, 1L);
-                final SearchCriteria<QuotaTariffVO> sc = 
listAllIncludedUsageType.create();
-                sc.setParameters("onorbefore", effectiveDate);
-                for (Integer quotaType : QuotaTypes.listQuotaTypes().keySet()) 
{
-                    sc.setParameters("quotatype", quotaType);
-                    List<QuotaTariffVO> result = search(sc, filter);
-                    if (result != null && !result.isEmpty()) {
-                        tariffs.add(result.get(0));
-                        if (logger.isDebugEnabled()) {
-                            logger.debug("ListAllTariffPlans on or before " + 
effectiveDate + " quota type " + result.get(0).getUsageTypeDescription() + " , 
effective Date="
-                                    + result.get(0).getEffectiveOn() + " val=" 
+ result.get(0).getCurrencyValue());
-                        }
-                    }
-                }
-                return new Pair<>(paginateList(tariffs, startIndex, pageSize), 
tariffs.size());
-            }
-        });
-    }
-
     @Override
     public Boolean updateQuotaTariff(final QuotaTariffVO plan) {
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new 
TransactionCallback<Boolean>() {
-            @Override
-            public Boolean doInTransaction(final TransactionStatus status) {
-                return update(plan.getId(), plan);
-            }
-        });
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<Boolean>) status -> update(plan.getId(), plan));
     }
 
     @Override
     public QuotaTariffVO addQuotaTariff(final QuotaTariffVO plan) {
         if (plan.getIdObj() != null) {
-            throw new IllegalStateException("The QuotaTariffVO being added 
should not have an Id set ");
+            throw new IllegalStateException("The QuotaTariffVO being added 
should not have an Id set.");
         }
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new 
TransactionCallback<QuotaTariffVO>() {
-            @Override
-            public QuotaTariffVO doInTransaction(final TransactionStatus 
status) {
-                return persist(plan);
-            }
-        });
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<QuotaTariffVO>) status -> persist(plan));
     }
 
     @Override
     public Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, 
Date endDate, Integer usageType, String name, String uuid, boolean listAll, 
Long startIndex, Long pageSize) {
-        SearchCriteria<QuotaTariffVO> searchCriteria = 
createListQuotaTariffsSearchCriteria(startDate, endDate, usageType, name, uuid);
+        return listQuotaTariffs(startDate, endDate, usageType, name, uuid, 
listAll, false, startIndex, pageSize, null);
+    }
+
+    @Override
+    public Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, 
Date endDate, Integer usageType, String name, String uuid, boolean listAll, 
boolean listOnlyRemoved, Long startIndex, Long pageSize, String keyword) {
+        SearchCriteria<QuotaTariffVO> searchCriteria = 
createListQuotaTariffsSearchCriteria(startDate, endDate, usageType, name, uuid, 
listOnlyRemoved, keyword);
+
         Filter sorter = new Filter(QuotaTariffVO.class, "usageType", false, 
startIndex, pageSize);
         sorter.addOrderBy(QuotaTariffVO.class, "effectiveOn", false);
         sorter.addOrderBy(QuotaTariffVO.class, "updatedOn", false);
@@ -166,39 +79,34 @@ public class QuotaTariffDaoImpl extends 
GenericDaoBase<QuotaTariffVO, Long> impl
         return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<Pair<List<QuotaTariffVO>, Integer>>) status -> 
searchAndCount(searchCriteria, sorter, listAll));
     }
 
-    protected SearchCriteria<QuotaTariffVO> 
createListQuotaTariffsSearchCriteria(Date startDate, Date endDate, Integer 
usageType, String name, String uuid) {
-        SearchCriteria<QuotaTariffVO> searchCriteria = 
createListQuotaTariffsSearchBuilder(startDate, endDate, usageType, name, 
uuid).create();
+    protected SearchCriteria<QuotaTariffVO> 
createListQuotaTariffsSearchCriteria(Date startDate, Date endDate, Integer 
usageType, String name, String uuid, boolean listOnlyRemoved, String keyword) {
+        SearchCriteria<QuotaTariffVO> searchCriteria = 
createListQuotaTariffsSearchBuilder(listOnlyRemoved).create();
 
-        searchCriteria.setParametersIfNotNull("start_date", startDate);
-        searchCriteria.setParametersIfNotNull("end_date", endDate);
-        searchCriteria.setParametersIfNotNull("usage_type", usageType);
+        searchCriteria.setParametersIfNotNull("startDate", startDate);
+        searchCriteria.setParametersIfNotNull("endDate", endDate);
+        searchCriteria.setParametersIfNotNull("usageType", usageType);
         searchCriteria.setParametersIfNotNull("name", name);
         searchCriteria.setParametersIfNotNull("uuid", uuid);
 
+        if (keyword != null) {
+            searchCriteria.setParameters("nameLike", "%" + keyword + "%");
+        }
+
         return searchCriteria;
     }
 
-    protected SearchBuilder<QuotaTariffVO> 
createListQuotaTariffsSearchBuilder(Date startDate, Date endDate, Integer 
usageType, String name, String uuid) {
+    protected SearchBuilder<QuotaTariffVO> 
createListQuotaTariffsSearchBuilder(boolean listOnlyRemoved) {
         SearchBuilder<QuotaTariffVO> searchBuilder = createSearchBuilder();
 
-        if (startDate != null) {
-            searchBuilder.and("start_date", 
searchBuilder.entity().getEffectiveOn(), SearchCriteria.Op.GTEQ);
-        }
-
-        if (endDate != null) {
-            searchBuilder.and("end_date", searchBuilder.entity().getEndDate(), 
SearchCriteria.Op.LTEQ);
-        }
-
-        if (usageType != null) {
-            searchBuilder.and("usage_type", 
searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ);
-        }
-
-        if (name != null) {
-            searchBuilder.and("name", searchBuilder.entity().getName(), 
SearchCriteria.Op.EQ);
-        }
+        searchBuilder.and("startDate", 
searchBuilder.entity().getEffectiveOn(), SearchCriteria.Op.GTEQ);
+        searchBuilder.and("endDate", searchBuilder.entity().getEndDate(), 
SearchCriteria.Op.LTEQ);
+        searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), 
SearchCriteria.Op.EQ);
+        searchBuilder.and("name", searchBuilder.entity().getName(), 
SearchCriteria.Op.EQ);
+        searchBuilder.and("uuid", searchBuilder.entity().getUuid(), 
SearchCriteria.Op.EQ);
+        searchBuilder.and("nameLike", searchBuilder.entity().getName(), 
SearchCriteria.Op.LIKE);
 
-        if (uuid != null) {
-            searchBuilder.and("uuid", searchBuilder.entity().getUuid(), 
SearchCriteria.Op.EQ);
+        if (listOnlyRemoved) {
+            searchBuilder.and("removed", searchBuilder.entity().getRemoved(), 
SearchCriteria.Op.NNULL);
         }
 
         return searchBuilder;
diff --git a/plugins/database/quota/pom.xml b/plugins/database/quota/pom.xml
index 9dada4128a5..b574b263020 100644
--- a/plugins/database/quota/pom.xml
+++ b/plugins/database/quota/pom.xml
@@ -62,5 +62,10 @@
             <artifactId>joda-time</artifactId>
             <version>${cs.joda-time.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.cloudstack</groupId>
+            <artifactId>cloud-plugin-api-discovery</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffCreateCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffCreateCmd.java
index 137f42536df..f1fd4b4afe1 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffCreateCmd.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffCreateCmd.java
@@ -54,10 +54,7 @@ public class QuotaTariffCreateCmd extends BaseCmd {
     @Parameter(name = "value", type = CommandType.DOUBLE, required = true, 
description = "The quota tariff value of the resource as per the default unit.")
     private Double value;
 
-    @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, 
description = "Quota tariff's activation rule. It can receive a JS script that 
results in either " +
-            "a boolean or a numeric value: if it results in a boolean value, 
the tariff value will be applied according to the result; if it results in a 
numeric value, the " +
-            "numeric value will be applied; if the result is neither a boolean 
nor a numeric value, the tariff will not be applied. If the rule is not 
informed, the tariff " +
-            "value will be applied.", length = 65535)
+    @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, 
description = ApiConstants.PARAMETER_DESCRIPTION_ACTIVATION_RULE, length = 
65535)
     private String activationRule;
 
     @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, 
description = "The effective start date on/after which the quota tariff is 
effective. Inform null to " +
@@ -80,7 +77,7 @@ public class QuotaTariffCreateCmd extends BaseCmd {
             throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed 
to create new quota tariff.");
         }
 
-        QuotaTariffResponse response = 
responseBuilder.createQuotaTariffResponse(result);
+        QuotaTariffResponse response = 
responseBuilder.createQuotaTariffResponse(result, true);
         response.setResponseName(getCommandName());
         setResponseObject(response);
     }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffListCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffListCmd.java
index b4e8c868e40..d054d545931 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffListCmd.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffListCmd.java
@@ -17,15 +17,18 @@
 package org.apache.cloudstack.api.command;
 
 import com.cloud.user.Account;
+import com.cloud.user.User;
 import com.cloud.utils.Pair;
 
 import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseListCmd;
 import org.apache.cloudstack.api.Parameter;
 import org.apache.cloudstack.api.response.ListResponse;
 import org.apache.cloudstack.api.response.QuotaResponseBuilder;
 import org.apache.cloudstack.api.response.QuotaTariffResponse;
+import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import 
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
 
@@ -59,20 +62,29 @@ public class QuotaTariffListCmd extends BaseListCmd {
             + "list all, including the removed ones. The default is false.", 
since = "4.18.0.0")
     private boolean listAll = false;
 
-    public QuotaTariffListCmd() {
-        super();
-    }
+    @Parameter(name = ApiConstants.LIST_ONLY_REMOVED, type = 
CommandType.BOOLEAN, description = "If set to true, we will list only the 
removed tariffs."
+            + " The default is false.")
+    private boolean listOnlyRemoved = false;
+
+    @Parameter(name = ApiConstants.ID, type = CommandType.STRING, description 
= "The quota tariff's id.", validations = {ApiArgValidator.UuidString})
+    private String id;
 
     @Override
     public void execute() {
         final Pair<List<QuotaTariffVO>, Integer> result = 
_responseBuilder.listQuotaTariffPlans(this);
 
+        User user = CallContext.current().getCallingUser();
+        boolean returnActivationRules = 
_responseBuilder.isUserAllowedToSeeActivationRules(user);
+        if (!returnActivationRules) {
+            logger.debug("User [{}] does not have permission to create or 
update quota tariffs, therefore we will not return the activation rules.", 
user.getUuid());
+        }
+
         final List<QuotaTariffResponse> responses = new ArrayList<>();
 
-        logger.trace(String.format("Adding quota tariffs [%s] to response of 
API quotaTariffList.", 
ReflectionToStringBuilderUtils.reflectCollection(responses)));
+        logger.trace("Adding quota tariffs [{}] to response of API 
quotaTariffList.", ReflectionToStringBuilderUtils.reflectCollection(responses));
 
         for (final QuotaTariffVO resource : result.first()) {
-            
responses.add(_responseBuilder.createQuotaTariffResponse(resource));
+            responses.add(_responseBuilder.createQuotaTariffResponse(resource, 
returnActivationRules));
         }
 
         final ListResponse<QuotaTariffResponse> response = new 
ListResponse<>();
@@ -106,4 +118,15 @@ public class QuotaTariffListCmd extends BaseListCmd {
         return listAll;
     }
 
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public boolean isListOnlyRemoved() {
+        return listOnlyRemoved;
+    }
 }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmd.java
index 6370cc57e4e..b5766875507 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmd.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmd.java
@@ -63,10 +63,8 @@ public class QuotaTariffUpdateCmd extends BaseCmd {
             since = "4.18.0.0")
     private String description;
 
-    @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, 
description = "Quota tariff's activation rule. It can receive a JS script that 
results in either " +
-            "a boolean or a numeric value: if it results in a boolean value, 
the tariff value will be applied according to the result; if it results in a 
numeric value, the " +
-            "numeric value will be applied; if the result is neither a boolean 
nor a numeric value, the tariff will not be applied. If the rule is not 
informed, the tariff " +
-            "value will be applied. Inform empty to remove the activation 
rule.", length = 65535, since = "4.18.0.0")
+    @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, 
description = ApiConstants.PARAMETER_DESCRIPTION_ACTIVATION_RULE +
+            " Inform empty to remove the activation rule.", length = 65535, 
since = "4.18.0.0")
     private String activationRule;
 
     @Parameter(name = ApiConstants.POSITION, type = CommandType.INTEGER, 
description = "Position in the execution sequence for tariffs of the same 
type", since = "4.20.0.0")
@@ -119,7 +117,7 @@ public class QuotaTariffUpdateCmd extends BaseCmd {
         if (result == null) {
             throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed 
to update quota tariff plan");
         }
-        final QuotaTariffResponse response = 
_responseBuilder.createQuotaTariffResponse(result);
+        final QuotaTariffResponse response = 
_responseBuilder.createQuotaTariffResponse(result, true);
         response.setResponseName(getCommandName());
         setResponseObject(response);
     }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
index ecbb809b60b..c635551aeb5 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
@@ -16,6 +16,7 @@
 //under the License.
 package org.apache.cloudstack.api.response;
 
+import com.cloud.user.User;
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
 import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
@@ -41,7 +42,9 @@ public interface QuotaResponseBuilder {
 
     Pair<List<QuotaTariffVO>, Integer> listQuotaTariffPlans(QuotaTariffListCmd 
cmd);
 
-    QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO configuration);
+    QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO quotaTariff, 
boolean returnActivationRule);
+
+    boolean isUserAllowedToSeeActivationRules(User user);
 
     QuotaStatementResponse createQuotaStatementResponse(List<QuotaUsageVO> 
quotaUsage);
 
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
index 88e90cc9ba9..1c486759e43 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
@@ -52,6 +52,7 @@ import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
 import org.apache.cloudstack.api.command.QuotaTariffListCmd;
 import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.discovery.ApiDiscoveryService;
 import org.apache.cloudstack.quota.QuotaManager;
 import org.apache.cloudstack.quota.QuotaManagerImpl;
 import org.apache.cloudstack.quota.QuotaService;
@@ -135,8 +136,11 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
 
     private final Class<?>[] assignableClasses = {GenericPresetVariable.class, 
ComputingResources.class};
 
+    @Inject
+    private ApiDiscoveryService apiDiscoveryService;
+
     @Override
-    public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff) 
{
+    public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, 
boolean returnActivationRule) {
         final QuotaTariffResponse response = new QuotaTariffResponse();
         response.setUsageType(tariff.getUsageType());
         response.setUsageName(tariff.getUsageName());
@@ -146,13 +150,15 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
         response.setEffectiveOn(tariff.getEffectiveOn());
         response.setUsageTypeDescription(tariff.getUsageTypeDescription());
         response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
-        response.setActivationRule(tariff.getActivationRule());
         response.setName(tariff.getName());
         response.setEndDate(tariff.getEndDate());
         response.setDescription(tariff.getDescription());
         response.setId(tariff.getUuid());
         response.setRemoved(tariff.getRemoved());
         response.setPosition(tariff.getPosition());
+        if (returnActivationRule) {
+            response.setActivationRule(tariff.getActivationRule());
+        }
         return response;
     }
 
@@ -228,6 +234,11 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
         }
     }
 
+    public boolean isUserAllowedToSeeActivationRules(User user) {
+        List<ApiDiscoveryResponse> apiList = (List<ApiDiscoveryResponse>) 
apiDiscoveryService.listApis(user, null).getResponses();
+        return apiList.stream().anyMatch(response -> 
StringUtils.equalsAny(response.getName(), "quotaTariffCreate", 
"quotaTariffUpdate"));
+    }
+
     @Override
     public QuotaBalanceResponse 
createQuotaBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate, 
Date endDate) {
         if (quotaBalance == null || quotaBalance.isEmpty()) {
@@ -400,11 +411,14 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
         boolean listAll = cmd.isListAll();
         Long startIndex = cmd.getStartIndex();
         Long pageSize = cmd.getPageSizeVal();
+        String uuid = cmd.getId();
+        boolean listOnlyRemoved = cmd.isListOnlyRemoved();
+        String keyword = cmd.getKeyword();
 
-        logger.debug(String.format("Listing quota tariffs for parameters 
[%s].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, 
"effectiveDate",
-                "endDate", "listAll", "name", "page", "pageSize", 
"usageType")));
+        logger.debug("Listing quota tariffs for parameters [{}].", 
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "effectiveDate",
+                "endDate", "listAll", "name", "page", "pageSize", "usageType", 
"uuid", "listOnlyRemoved", "keyword"));
 
-        return _quotaTariffDao.listQuotaTariffs(startDate, endDate, usageType, 
name, null, listAll, startIndex, pageSize);
+        return _quotaTariffDao.listQuotaTariffs(startDate, endDate, usageType, 
name, uuid, listAll, listOnlyRemoved, startIndex, pageSize, keyword);
     }
 
     @Override
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffListCmdTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffListCmdTest.java
index f5ce92ae014..a98d3d611de 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffListCmdTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffListCmdTest.java
@@ -16,15 +16,18 @@
 // under the License.
 package org.apache.cloudstack.api.command;
 
+import com.cloud.user.User;
 import junit.framework.TestCase;
 import org.apache.cloudstack.api.response.QuotaResponseBuilder;
 import org.apache.cloudstack.api.response.QuotaTariffResponse;
+import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.quota.constant.QuotaTypes;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.MockedStatic;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.lang.reflect.Field;
@@ -40,6 +43,12 @@ public class QuotaTariffListCmdTest extends TestCase {
     @Mock
     QuotaResponseBuilder responseBuilder;
 
+    @Mock
+    User userMock;
+
+    @Mock
+    CallContext callContextMock;
+
     @Test
     public void testQuotaTariffListCmd() throws NoSuchFieldException, 
IllegalAccessException {
         QuotaTariffListCmd cmd = new QuotaTariffListCmd();
@@ -48,17 +57,24 @@ public class QuotaTariffListCmdTest extends TestCase {
         rbField.setAccessible(true);
         rbField.set(cmd, responseBuilder);
 
-        List<QuotaTariffVO> quotaTariffVOList = new ArrayList<QuotaTariffVO>();
+        List<QuotaTariffVO> quotaTariffVOList = new ArrayList<>();
         QuotaTariffVO tariff = new QuotaTariffVO();
         tariff.setEffectiveOn(new Date());
         tariff.setCurrencyValue(new BigDecimal(100));
         tariff.setUsageType(QuotaTypes.VOLUME);
 
         quotaTariffVOList.add(new QuotaTariffVO());
-        
Mockito.when(responseBuilder.listQuotaTariffPlans(Mockito.eq(cmd))).thenReturn(new
 Pair<>(quotaTariffVOList, quotaTariffVOList.size()));
-        
Mockito.when(responseBuilder.createQuotaTariffResponse(Mockito.any(QuotaTariffVO.class))).thenReturn(new
 QuotaTariffResponse());
 
-        cmd.execute();
-        Mockito.verify(responseBuilder, 
Mockito.times(1)).createQuotaTariffResponse(Mockito.any(QuotaTariffVO.class));
+        try (MockedStatic<CallContext> callContextStaticMock = 
Mockito.mockStatic(CallContext.class)) {
+            
Mockito.when(responseBuilder.listQuotaTariffPlans(Mockito.eq(cmd))).thenReturn(new
 Pair<>(quotaTariffVOList, quotaTariffVOList.size()));
+            
callContextStaticMock.when(CallContext::current).thenReturn(callContextMock);
+            
Mockito.when(callContextMock.getCallingUser()).thenReturn(userMock);
+            
Mockito.when(responseBuilder.isUserAllowedToSeeActivationRules(userMock)).thenReturn(true);
+            
Mockito.when(responseBuilder.createQuotaTariffResponse(Mockito.any(QuotaTariffVO.class),
 Mockito.eq(true))).thenReturn(new QuotaTariffResponse());
+
+            cmd.execute();
+        }
+
+        Mockito.verify(responseBuilder, 
Mockito.times(1)).createQuotaTariffResponse(Mockito.any(QuotaTariffVO.class), 
Mockito.eq(true));
     }
 }
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmdTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmdTest.java
index 22d78d6794e..7a4d1a75356 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmdTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaTariffUpdateCmdTest.java
@@ -60,8 +60,8 @@ public class QuotaTariffUpdateCmdTest extends TestCase {
         }
 
         
Mockito.when(responseBuilder.updateQuotaTariffPlan(Mockito.eq(cmd))).thenReturn(tariff);
-        
Mockito.when(responseBuilder.createQuotaTariffResponse(Mockito.eq(tariff))).thenReturn(new
 QuotaTariffResponse());
+        
Mockito.when(responseBuilder.createQuotaTariffResponse(Mockito.eq(tariff), 
Mockito.eq(true))).thenReturn(new QuotaTariffResponse());
         cmd.execute();
-        Mockito.verify(responseBuilder, 
Mockito.times(1)).createQuotaTariffResponse(Mockito.eq(tariff));
+        Mockito.verify(responseBuilder, 
Mockito.times(1)).createQuotaTariffResponse(Mockito.eq(tariff), 
Mockito.eq(true));
     }
 }
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
index 71e38a5ab8c..fd359525893 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
@@ -55,7 +55,10 @@ import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
+import org.apache.cloudstack.discovery.ApiDiscoveryService;
+
 import org.apache.commons.lang3.time.DateUtils;
+
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -69,6 +72,7 @@ import com.cloud.user.Account;
 import com.cloud.user.AccountVO;
 import com.cloud.user.dao.AccountDao;
 import com.cloud.user.dao.UserDao;
+import com.cloud.user.User;
 
 import junit.framework.TestCase;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -91,6 +95,12 @@ public class QuotaResponseBuilderImplTest extends TestCase {
     @Mock
     UserDao userDaoMock;
 
+    @Mock
+    User userMock;
+
+    @Mock
+    ApiDiscoveryService discoveryServiceMock;
+
     @Mock
     QuotaService quotaServiceMock;
 
@@ -164,11 +174,29 @@ public class QuotaResponseBuilderImplTest extends 
TestCase {
     @Test
     public void testQuotaResponse() {
         QuotaTariffVO tariffVO = makeTariffTestData();
-        QuotaTariffResponse response = 
quotaResponseBuilderSpy.createQuotaTariffResponse(tariffVO);
+        QuotaTariffResponse response = 
quotaResponseBuilderSpy.createQuotaTariffResponse(tariffVO, true);
         assertTrue(tariffVO.getUsageType() == response.getUsageType());
         
assertTrue(tariffVO.getCurrencyValue().equals(response.getTariffValue()));
     }
 
+    @Test
+    public void 
createQuotaTariffResponseTestIfReturnsActivationRuleWithPermission() {
+        QuotaTariffVO tariff = makeTariffTestData();
+        tariff.setActivationRule("x === 10");
+
+        QuotaTariffResponse tariffResponse = 
quotaResponseBuilderSpy.createQuotaTariffResponse(tariff, true);
+        assertEquals("x === 10", tariffResponse.getActivationRule());
+    }
+
+    @Test
+    public void 
createQuotaTariffResponseTestIfReturnsActivationRuleWithoutPermission() {
+        QuotaTariffVO tariff = makeTariffTestData();
+        tariff.setActivationRule("x === 10");
+
+        QuotaTariffResponse tariffResponse = 
quotaResponseBuilderSpy.createQuotaTariffResponse(tariff, false);
+        assertNull(tariffResponse.getActivationRule());
+    }
+
     @Test
     public void testAddQuotaCredits() {
         final long accountId = 2L;
@@ -569,4 +597,52 @@ public class QuotaResponseBuilderImplTest extends TestCase 
{
         Mockito.verify(quotaTariffVoMock).setPosition(position);
     }
 
+
+    @Test
+    public void 
isUserAllowedToSeeActivationRulesTestWithPermissionToCreateTariff() {
+        ApiDiscoveryResponse response = new ApiDiscoveryResponse();
+        response.setName("quotaTariffCreate");
+
+        List<ApiDiscoveryResponse> cmdList = new ArrayList<>();
+        cmdList.add(response);
+
+        ListResponse<ApiDiscoveryResponse> responseList = new ListResponse<>();
+        responseList.setResponses(cmdList);
+
+        
Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, 
null);
+
+        
assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
+    }
+
+    @Test
+    public void 
isUserAllowedToSeeActivationRulesTestWithPermissionToUpdateTariff() {
+        ApiDiscoveryResponse response = new ApiDiscoveryResponse();
+        response.setName("quotaTariffUpdate");
+
+        List<ApiDiscoveryResponse> cmdList = new ArrayList<>();
+        cmdList.add(response);
+
+        ListResponse<ApiDiscoveryResponse> responseList = new ListResponse<>();
+        responseList.setResponses(cmdList);
+
+        
Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, 
null);
+
+        
assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
+    }
+
+    @Test
+    public void isUserAllowedToSeeActivationRulesTestWithNoPermission() {
+        ApiDiscoveryResponse response = new ApiDiscoveryResponse();
+        response.setName("testCmd");
+
+        List<ApiDiscoveryResponse> cmdList = new ArrayList<>();
+        cmdList.add(response);
+
+        ListResponse<ApiDiscoveryResponse> responseList = new ListResponse<>();
+        responseList.setResponses(cmdList);
+
+        
Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, 
null);
+
+        
assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
+    }
 }
diff --git a/server/src/main/java/com/cloud/usage/UsageServiceImpl.java 
b/server/src/main/java/com/cloud/usage/UsageServiceImpl.java
index 170ef1fdbbc..421d2587441 100644
--- a/server/src/main/java/com/cloud/usage/UsageServiceImpl.java
+++ b/server/src/main/java/com/cloud/usage/UsageServiceImpl.java
@@ -31,7 +31,6 @@ import com.cloud.utils.DateUtil;
 import org.apache.cloudstack.api.command.admin.usage.GenerateUsageRecordsCmd;
 import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd;
 import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd;
-import org.apache.cloudstack.api.response.UsageTypeResponse;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.usage.Usage;
@@ -485,10 +484,4 @@ public class UsageServiceImpl extends ManagerBase 
implements UsageService, Manag
         }
         return true;
     }
-
-    @Override
-    public List<UsageTypeResponse> listUsageTypes() {
-        return UsageTypes.listUsageTypes();
-    }
-
 }
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 166097778c0..b42899fc631 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -161,6 +161,9 @@
 "label.action.patch.systemvm.processing": "Patching System VM....",
 "label.action.project.add.account": "Add Account to project",
 "label.action.project.add.user": "Add User to project",
+"label.action.quota.tariff.create": "Create Quota Tariff",
+"label.action.quota.tariff.edit": "Edit Quota Tariff",
+"label.action.quota.tariff.remove": "Remove Quota Tariff",
 "label.action.reboot.instance": "Reboot Instance",
 "label.action.reboot.router": "Reboot router",
 "label.action.reboot.systemvm": "Reboot System VM",
@@ -865,6 +868,7 @@
 "label.encrypt": "Encrypt",
 "label.encryptroot": "Encrypt Root Disk",
 "label.end": "End",
+"label.end.date": "End date",
 "label.end.date.and.time": "End date and time",
 "label.end.ip": "End IP",
 "label.end.reserved.system.ip": "End reserved system IP",
@@ -1727,6 +1731,8 @@
 "label.quota.summary": "Summary",
 "label.quota.tariff": "Tariff",
 "label.quota.tariff.effectivedate": "Effective date",
+"label.quota.tariff.position": "Position",
+"label.quota.tariff.value": "Tariff value",
 "label.quota.total": "Total",
 "label.quota.type.name": "Usage Type",
 "label.quota.type.unit": "Usage unit",
@@ -2060,6 +2066,7 @@
 "label.sslverification": "SSL verification",
 "label.standard.us.keyboard": "Standard (US) keyboard",
 "label.start": "Start",
+"label.start.date": "Start date",
 "label.start.date.and.time": "Start date and time",
 "label.start.ip": "Start IP",
 "label.start.lb.vm": "Start LB Instance",
@@ -2586,6 +2593,10 @@
 "message.action.primary.storage.scope.cluster": "Please confirm that you want 
to change the scope from zone to the specified cluster.<br>This operation will 
update the database and disconnect the storage pool from all hosts that were 
previously connected to the primary storage and are not part of the specified 
cluster.",
 "message.action.primary.storage.scope.zone": "Please confirm that you want to 
change the scope from cluster to zone.<br>This operation will update the 
database and connect the storage pool to all hosts of the zone running the same 
hypervisor as set on the storage pool.",
 "message.action.primarystorage.enable.maintenance.mode": "Warning: placing the 
primary storage into maintenance mode will cause all Instances using volumes 
from it to be stopped.  Do you want to continue?",
+"message.action.quota.tariff.create.error.namerequired": "Please, inform a 
name for the quota tariff.",
+"message.action.quota.tariff.create.error.usagetyperequired": "Please, select 
the usage type of the quota tariff.",
+"message.action.quota.tariff.create.error.valuerequired": "Please, inform a 
value for the quota tariff.",
+"message.action.quota.tariff.remove": "Please confirm that you want to remove 
this Quota Tariff.",
 "message.action.reboot.instance": "Please confirm that you want to reboot this 
Instance.",
 "message.action.reboot.router": "All services provided by this virtual router 
will be interrupted. Please confirm that you want to reboot this router.",
 "message.action.reboot.systemvm": "Please confirm that you want to reboot this 
system VM.",
@@ -3194,6 +3205,8 @@
 "message.protocol.description": "For XenServer, choose NFS, iSCSI, or 
PreSetup. For KVM, choose NFS, SharedMountPoint, RDB, CLVM or Gluster. For 
vSphere, choose NFS, PreSetup (VMFS or iSCSI or FiberChannel or vSAN or vVols) 
or DatastoreCluster. For Hyper-V, choose SMB/CIFS. For LXC, choose NFS or 
SharedMountPoint. For OVM, choose NFS or OCFS2.",
 "message.public.traffic.in.advanced.zone": "Public traffic is generated when 
Instances in the cloud access the internet. Publicly-accessible IPs must be 
allocated for this purpose. End Users can use the CloudStack UI to acquire 
these IPs to implement NAT between their guest Network and their public 
Network.<br/><br/>Provide at least one range of IP addresses for internet 
traffic.",
 "message.public.traffic.in.basic.zone": "Public traffic is generated when 
Instances in the cloud access the Internet or provide services to clients over 
the Internet. Publicly accessible IPs must be allocated for this purpose. When 
a Instance is created, an IP from this set of Public IPs will be allocated to 
the Instance in addition to the guest IP address. Static 1-1 NAT will be set up 
automatically between the public IP and the guest IP. End Users can also use 
the CloudStack UI to acqu [...]
+"message.quota.tariff.create.success": "Successfully created quota tariff 
\"{quotaTariff}\"",
+"message.quota.tariff.update.success": "Successfully updated quota tariff 
\"{quotaTariff}\"",
 "message.read.accept.license.agreements": "Please read and accept the terms 
for the license agreements.",
 "message.read.admin.guide.scaling.up": "Please read the dynamic scaling 
section in the admin guide before scaling up.",
 "message.recover.vm": "Please confirm that you would like to recover this 
Instance.",
@@ -3522,6 +3535,13 @@
 "migrate.from": "Migrate from",
 "migrate.to": "Migrate to",
 "migrationPolicy": "Migration policy",
+"placeholder.quota.tariff.description": "Quota tariff's description",
+"placeholder.quota.tariff.enddate": "Quota tariff's end date",
+"placeholder.quota.tariff.name": "Quota tariff's name",
+"placeholder.quota.tariff.position": "Quota tariff's position in the execution 
sequence",
+"placeholder.quota.tariff.startdate": "Quota tariff's start date",
+"placeholder.quota.tariff.usagetype": "Quota tariff's usage type",
+"placeholder.quota.tariff.value": "Quota tariff's value",
 "router.health.checks": "Health check",
 "side.by.side": "Side by Side",
 "state.completed": "Completed",
@@ -3543,5 +3563,32 @@
 "state.stopping": "Stopping",
 "state.suspended": "Suspended",
 "user.login": "Login",
-"user.logout": "Logout"
+"user.logout": "Logout",
+"ALLOCATED_VM": "Allocated VM",
+"BACKUP": "Backup",
+"BACKUP_OBJECT": "Backup Object",
+"IP_ADDRESS": "IP Address",
+"LOAD_BALANCER_POLICY": "Load Balancer Policy",
+"NETWORK": "Network",
+"NETWORK_BYTES_RECEIVED": "Network Bytes Received",
+"NETWORK_BYTES_SENT": "Network Bytes Sent",
+"NETWORK_OFFERING": "Network Offering",
+"RUNNING_VM": "Running VM",
+"PORT_FORWARDING_RULE": "Port Forwarding Rule",
+"SECURITY_GROUP": "Security Group",
+"SNAPSHOT": "Snapshot",
+"TEMPLATE": "Template",
+"VM_DISK_BYTES_READ": "VM Disk (Bytes Read)",
+"VM_DISK_BYTES_WRITE": "VM Disk (Bytes Write)",
+"VM_DISK_IO_READ": "VM Disk (IO Read)",
+"VM_DISK_IO_WRITE": "VM Disk (IO Write)",
+"VM_SNAPSHOT": "VM Snapshot",
+"VM_SNAPSHOT_ON_PRIMARY": "VM Snapshot on Primary",
+"VOLUME": "Volume",
+"VOLUME_SECONDARY": "Volume on Secondary",
+"VPN_USERS": "VPN Users",
+"Compute*Month": "Compute * Month",
+"GB*Month": "GB * Month",
+"IP*Month": "IP * Month",
+"Policy*Month": "Policy * Month"
 }
diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json
index fec66ba4cef..82d527ae4c1 100644
--- a/ui/public/locales/pt_BR.json
+++ b/ui/public/locales/pt_BR.json
@@ -162,6 +162,7 @@
 "label.action.vmsnapshot.revert": "Reverter snapshot de VM",
 "label.action.vmstoragesnapshot.create": "Criar snapshot de volume da VM",
 "label.actions": "A\u00e7\u00f5es",
+"label.active": "Ativo",
 "label.activate.project": "Ativar projeto",
 "label.activeviewersessions": "Sess\u00f5es ativas",
 "label.add": "Adicionar",
@@ -625,6 +626,7 @@
 "label.enable.vpc.offering": "Habilitar oferta VPC",
 "label.enable.vpn": "Habilitar VPN",
 "label.end": "Fim",
+"label.end.date": "Data de término",
 "label.end.date.and.time": "Data e hor\u00e1rio final",
 "label.end.ip": "IP final",
 "label.end.reserved.system.ip": "Fim dos IPs reservados para o sistema",
@@ -1279,7 +1281,12 @@
 "label.quotastate": "Estado da cota",
 "label.summary": "Sum\u00e1rio",
 "label.quota.tariff": "Tarifa",
+"label.action.quota.tariff.create": "Criar tarifa",
+"label.action.quota.tariff.edit": "Editar tarifa",
+"label.action.quota.tariff.remove": "Remover tarifa",
 "label.quota.tariff.effectivedate": "Data efetiva",
+"label.quota.tariff.position": "Posi\u00e7\u00e3o",
+"label.quota.tariff.value": "Valor",
 "label.quota.total": "Total",
 "label.quota.type.name": "Tipo de uso",
 "label.quota.type.unit": "Unidade do uso",
@@ -1338,6 +1345,7 @@
 "label.remove.vmware.datacenter": "Remover datacenter VMware",
 "label.remove.vpc": "Remover VPC",
 "label.remove.vpc.offering": "Remover oferta VPC",
+"label.removed": "Removido",
 "label.removing": "Removendo",
 "label.replace.acl": "Substituir ACL",
 "label.replace.acl.list": "Substituir lista ACL",
@@ -1518,6 +1526,7 @@
 "label.standard.us.keyboard": "Teclado padr\u00e3o (EUA)",
 "label.sslcertificates": "Certificados SSL",
 "label.start": "Iniciar",
+"label.start.date": "Data de in\u00edcio",
 "label.start.date.and.time": "Data e hor\u00e1rio inicial",
 "label.start.ip": "IP do in\u00edcio",
 "label.start.lb.vm": "Iniciar VM LB",
@@ -1674,7 +1683,7 @@
 "label.upload.volume.from.url": "Carregar volume por URL",
 "label.url": "URL",
 "label.usageinterface": "Interface de uso",
-"label.usagename": "Tipo",
+"label.usagetype": "Tipo",
 "label.usageunit": "Unidade",
 "label.use.kubectl.access.cluster": "os arquivos <code><b>kubectl</b></code> e 
<code><b>kubeconfig</b></code> para acessar o cluster",
 "label.use.local.timezone": "Use o fuso hor\u00e1rio local",
@@ -1858,6 +1867,10 @@
 "message.action.instance.reset.password": "Por favor confirme que voc\u00ea 
deseja alterar a senha de root para est\u00e1 m\u00e1quina virtual.",
 "message.action.manage.cluster": "Confirma a vincula\u00e7\u00e3o do cluster.",
 "message.action.primarystorage.enable.maintenance.mode": "Aviso: colocar o 
armazenamento prim\u00e1rio em modo de manuten\u00e7\u00e3o ir\u00e1 causar a 
parada de todas as VMs hospedadas nesta unidade. Deseja continuar?",
+"message.action.quota.tariff.create.error.namerequired": "Por favor, informe o 
nome da tarifa.",
+"message.action.quota.tariff.create.error.usagetyperequired": "Por favor, 
selecione o tipo da tarifa.",
+"message.action.quota.tariff.create.error.valuerequired": "Por favor, informe 
o valor da tarifa.",
+"message.action.quota.tariff.remove": "Por favor, confirme que voc\u00ea 
deseja remover a tarifa.",
 "message.action.reboot.instance": "Por favor, confirme que voc\u00ea deseja 
reiniciar esta inst\u00e2ncia.",
 "message.action.reboot.router": "Confirme que voc\ufffd deseja reiniciar este 
roteador.",
 "message.action.reboot.systemvm": "Confirme que voc\u00ea deseja reiniciar 
esta VM de sistema.",
@@ -2280,6 +2293,8 @@
 "message.protocol.description": "Para XenServer, escolha NFS, iSCSI, ou 
PreSetup. para KVM, escolha NFS, SharedMountPoint, RDB, CLVM ou Gluster. para 
vSphere, escolha NFS, PreSetup (VMFS, iSCSI, fiberChannel, vSAN ou vVols) ou 
datastoreCluster. para Hyper-V, escolha SMB/CIFS. para LXC, escolha NFS ou 
SharedMountPoint. para OVM, escolha NFS ou ocfs2.",
 "message.public.traffic.in.advanced.zone": "O tr\u00e1fego p\u00fablico \u00e9 
gerado quando as VMs na nuvem acessam a internet. Os IPs acess\u00edveis ao 
p\u00fablico devem ser alocados para essa finalidade. Os usu\u00e1rios finais 
podem usar a interface do usu\u00e1rio CloudStack para adquirir esses IPs afim 
de implementar NAT entre a sua rede de guests e sua rede p\u00fablica. 
<br/><br/> Forne\u00e7a pelo menos um intervalo de endere\u00e7os IP para o 
tr\u00e1fego de internet.",
 "message.public.traffic.in.basic.zone": "O tr\u00e1fego p\u00fablico \u00e9 
gerado quando as VMs na nuvem acessam a internet ou prestam servi\u00e7os aos 
clientes atrav\u00e9s da internet. Os IPs acess\u00edveis ao p\u00fablico devem 
ser alocados para essa finalidade. Quando uma inst\u00e2ncia \u00e9 criada, um 
IP a partir deste conjunto de IPs P\u00fablicos ser\u00e3o destinados \u00e0 
inst\u00e2ncia, al\u00e9m do endere\u00e7o IP guest. Um NAT est\u00e1tico 1-1 
ser\u00e1 criada automat [...]
+"message.quota.tariff.create.success": "Tarifa \"{quotaTariff}\" criada com 
sucesso",
+"message.quota.tariff.update.success": "Tarifa \"{quotaTariff}\" atualizada 
com sucesso",
 "message.read.accept.license.agreements": "Leia e aceite os termos dos 
contratos de licen\u00e7a.",
 "message.read.admin.guide.scaling.up": "Por favor leia a sess\u00e3o sobre 
escalonamento din\u00e2mico no guia do administrador antes de escalonar.",
 "message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.",
@@ -2495,6 +2510,13 @@
 "migrate.from": "Migrar de",
 "migrate.to": "Migrar para",
 "migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o",
+"placeholder.quota.tariff.description": "Descri\u00e7\u00e3o",
+"placeholder.quota.tariff.enddate": "Data de t\u00e9rmino",
+"placeholder.quota.tariff.name": "Nome",
+"placeholder.quota.tariff.position": "Posi\u00e7\u00e3o da tarifa do Quota na 
sequ\u00eancia de execu\u00e7\u00e3o",
+"placeholder.quota.tariff.startdate": "Data de in\u00edcio",
+"placeholder.quota.tariff.usagetype": "Tipo",
+"placeholder.quota.tariff.value": "Valor",
 "router.health.checks": "Checagem de sa\u00fade",
 "side.by.side": "Lado a lado",
 "state.completed": "Completo",
@@ -2516,5 +2538,32 @@
 "state.stopping": "Parando",
 "state.suspended": "Suspendido",
 "user.login": "Entrar",
-"user.logout": "Sair"
+"user.logout": "Sair",
+"ALLOCATED_VM": "VM alocada",
+"BACKUP": "Backup",
+"BACKUP_OBJECT": "Objeto backup",
+"IP_ADDRESS": "Endere\u00e7o IP",
+"LOAD_BALANCER_POLICY": "Pol\u00edtica de balanceamento de carga",
+"NETWORK": "Rede",
+"NETWORK_BYTES_RECEIVED": "Bytes recebidos na rede",
+"NETWORK_BYTES_SENT": "Bytes enviados na rede",
+"NETWORK_OFFERING": "Oferta de rede",
+"RUNNING_VM": "VM rodando",
+"PORT_FORWARDING_RULE": "Regra de redirecionamento de porta",
+"SECURITY_GROUP": "Grupo de seguran\u00e7a",
+"SNAPSHOT": "Snapshot",
+"TEMPLATE": "Template",
+"VM_DISK_BYTES_READ": "Leitura do disco da VM (bytes)",
+"VM_DISK_BYTES_WRITE": "Escrita no disco da VM (bytes)",
+"VM_DISK_IO_READ": "Leitura do disco da VM (IO)",
+"VM_DISK_IO_WRITE": "Escrita no disco da VM (IO)",
+"VM_SNAPSHOT": "Snapshot de VM",
+"VM_SNAPSHOT_ON_PRIMARY": "Snapshot de VM no armazenamento prim\u00e1rio",
+"VOLUME": "Volume",
+"VOLUME_SECONDARY": "Volume no armazenamento secund\u00e1rio",
+"VPN_USERS": "Usu\u00e1rios de VPN",
+"Compute*Month": "Recurso * M\u00eas",
+"GB*Month": "GB * M\u00eas",
+"IP*Month": "IP * M\u00eas",
+"Policy*Month": "Pol\u00edticas de Rede * M\u00eas"
 }
diff --git a/ui/src/components/view/DetailsTab.vue 
b/ui/src/components/view/DetailsTab.vue
index c9ab6b89ec8..017d304e39b 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -38,8 +38,8 @@
     :dataSource="fetchDetails()">
     <template #renderItem="{item}">
       <a-list-item v-if="(item in dataResource && 
!customDisplayItems.includes(item)) || (offeringDetails.includes(item) && 
dataResource.serviceofferingdetails)">
-        <div>
-          <strong>{{ item === 'service' ? $t('label.supportedservices') : 
$t('label.' + String(item).toLowerCase()) }}</strong>
+        <div style="width: 100%">
+          <strong>{{ item === 'service' ? $t('label.supportedservices') : 
$t(getDetailTitle(item)) }}</strong>
           <br/>
           <div v-if="Array.isArray(dataResource[item]) && item === 'service'">
             <div v-for="(service, idx) in dataResource[item]" :key="idx">
@@ -84,9 +84,10 @@
             <span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 
'FIREWALL.CLOSE', 
'ALERT.SERVICE.DOMAINROUTER'].includes(dataResource[item])">{{ 
$t(dataResource[item].toLowerCase()) }}</span>
             <span v-else>{{ dataResource[item] }}</span>
           </div>
-          <div v-else-if="['created', 'sent', 'lastannotated', 
'collectiontime', 'lastboottime', 'lastserverstart', 
'lastserverstop'].includes(item)">
+          <div v-else-if="['created', 'sent', 'lastannotated', 
'collectiontime', 'lastboottime', 'lastserverstart', 'lastserverstop', 
'removed', 'effectiveDate', 'endDate'].includes(item)">
             {{ $toLocaleDate(dataResource[item]) }}
           </div>
+          <div style="white-space: pre-wrap;" v-else-if="$route.meta.name === 
'quotatariff' && item === 'description'">{{ dataResource[item] }}</div>
           <div v-else-if="$route.meta.name === 'userdata' && item === 
'userdata'">
             <div style="white-space: pre-wrap;"> {{ 
decodeUserData(dataResource.userdata)}} </div>
           </div>
@@ -179,7 +180,8 @@ export default {
       dedicatedRoutes: ['zone', 'pod', 'cluster', 'host'],
       dedicatedSectionActive: false,
       projectname: '',
-      dataResource: {}
+      dataResource: {},
+      detailsTitles: []
     }
   },
   mounted () {
@@ -342,12 +344,33 @@ export default {
       this.dataResource.account = projectAdmins.join()
     },
     fetchDetails () {
-      var details = this.$route.meta.details
+      let details = this.$route.meta.details
+
+      if (!details) {
+        return
+      }
+
       if (typeof details === 'function') {
         details = details()
       }
-      details = this.projectname ? [...details.filter(x => x !== 'account'), 
'projectname'] : details
-      return details
+
+      let detailsKeys = []
+      for (const detail of details) {
+        if (typeof detail === 'object') {
+          const field = detail.field
+          detailsKeys.push(field)
+          this.detailsTitles[field] = detail.customTitle
+        } else {
+          detailsKeys.push(detail)
+          this.detailsTitles[detail] = detail
+        }
+      }
+
+      detailsKeys = this.projectname ? [...detailsKeys.filter(x => x !== 
'account'), 'projectname'] : detailsKeys
+      return detailsKeys
+    },
+    getDetailTitle (detail) {
+      return `label.${String(this.detailsTitles[detail]).toLowerCase()}`
     }
   }
 }
diff --git a/ui/src/components/view/ListView.vue 
b/ui/src/components/view/ListView.vue
index 68e4bc0ffca..341bdffc918 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -372,7 +372,7 @@
         <status :text="record.enabled ? record.enabled.toString() : 'false'" />
         {{ record.enabled ? 'Enabled' : 'Disabled' }}
       </template>
-      <template v-if="['created', 'sent'].includes(column.key) || 
(['startdate'].includes(column.key) && 
['webhook'].includes($route.path.split('/')[1]))">
+      <template v-if="['created', 'sent', 'removed', 'effectiveDate', 
'endDate'].includes(column.key) || (['startdate'].includes(column.key) && 
['webhook'].includes($route.path.split('/')[1]))">
         {{ $toLocaleDate(text) }}
       </template>
       <template v-if="['startdate', 'enddate'].includes(column.key) && ['vm', 
'vnfapp'].includes($route.path.split('/')[1])">
@@ -675,7 +675,7 @@ export default {
         '/project', '/account', 'buckets', 'objectstore',
         '/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', 
'/systemvm', '/router', '/ilbvm', '/annotation',
         '/computeoffering', '/systemoffering', '/diskoffering', 
'/backupoffering', '/networkoffering', '/vpcoffering',
-        '/tungstenfabric', '/oauthsetting', '/guestos', 
'/guestoshypervisormapping', '/webhook', 'webhookdeliveries'].join('|'))
+        '/tungstenfabric', '/oauthsetting', '/guestos', 
'/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 
'/quotatariff'].join('|'))
         .test(this.$route.path)
     },
     enableGroupAction () {
@@ -970,7 +970,7 @@ export default {
       col.width = w
     },
     updateSelectedColumns (name) {
-      this.$emit('update-selected-columns', name)
+      this.$emit('update-selected-columns', this.getColumnKey(name))
     },
     getVmRouteUsingType (record) {
       switch (record.virtualmachinetype) {
@@ -999,7 +999,7 @@ export default {
           if (json && json.listusagetypesresponse && 
json.listusagetypesresponse.usagetype) {
             this.usageTypes = json.listusagetypesresponse.usagetype.map(x => {
               return {
-                id: x.usagetypeid,
+                id: x.id,
                 value: x.description
               }
             })
diff --git a/ui/src/components/view/SearchView.vue 
b/ui/src/components/view/SearchView.vue
index f642ddf938d..7883b162d66 100644
--- a/ui/src/components/view/SearchView.vue
+++ b/ui/src/components/view/SearchView.vue
@@ -162,6 +162,7 @@ import { isAdmin } from '@/role'
 import TooltipButton from '@/components/widgets/TooltipButton'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import Status from '@/components/widgets/Status'
+import { i18n } from '@/locales'
 
 export default {
   name: 'SearchView',
@@ -290,9 +291,13 @@ export default {
         if (item === 'groupid' && !('listInstanceGroups' in 
this.$store.getters.apis)) {
           return true
         }
+        if (item === 'usagetype' && !('listUsageTypes' in 
this.$store.getters.apis)) {
+          return true
+        }
+
         if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 
'account', 'hypervisor', 'level',
           'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 
'systemvmtype', 'scope', 'provider',
-          'type', 'scope', 'managementserverid', 'serviceofferingid', 
'diskofferingid'].includes(item)
+          'type', 'scope', 'managementserverid', 'serviceofferingid', 
'diskofferingid', 'usagetype'].includes(item)
         ) {
           type = 'list'
         } else if (item === 'tags') {
@@ -414,6 +419,7 @@ export default {
       let managementServerIdIndex = -1
       let serviceOfferingIndex = -1
       let diskOfferingIndex = -1
+      let usageTypeIndex = -1
 
       if (arrayField.includes('type')) {
         if (this.$route.path === '/alert') {
@@ -499,6 +505,12 @@ export default {
         promises.push(await this.fetchDiskOfferings(searchKeyword))
       }
 
+      if (arrayField.includes('usagetype')) {
+        usageTypeIndex = this.fields.findIndex(item => item.name === 
'usagetype')
+        this.fields[usageTypeIndex].loading = true
+        promises.push(await this.fetchUsageTypes())
+      }
+
       Promise.all(promises).then(response => {
         if (typeIndex > -1) {
           const types = response.filter(item => item.type === 'type')
@@ -581,6 +593,12 @@ export default {
             this.fields[diskOfferingIndex].opts = 
this.sortArray(diskOfferings[0].data)
           }
         }
+        if (usageTypeIndex > -1) {
+          const usageTypes = response.filter(item => item.type === 'usagetype')
+          if (usageTypes?.length > 0) {
+            this.fields[usageTypeIndex].opts = 
this.sortArray(usageTypes[0].data)
+          }
+        }
       }).finally(() => {
         if (typeIndex > -1) {
           this.fields[typeIndex].loading = false
@@ -618,6 +636,9 @@ export default {
         if (diskOfferingIndex > -1) {
           this.fields[diskOfferingIndex].loading = false
         }
+        if (usageTypeIndex > -1) {
+          this.fields[usageTypeIndex].loading = false
+        }
         if (Array.isArray(arrayField)) {
           this.fillFormFieldValues()
         }
@@ -1165,6 +1186,27 @@ export default {
       })
       return levels
     },
+    fetchUsageTypes () {
+      return new Promise((resolve, reject) => {
+        api('listUsageTypes')
+          .then(json => {
+            const usageTypes = json.listusagetypesresponse.usagetype.map(entry 
=> {
+              return {
+                id: entry.id,
+                name: i18n.global.t(entry.name)
+              }
+            })
+
+            resolve({
+              type: 'usagetype',
+              data: usageTypes
+            })
+          })
+          .catch(error => {
+            reject(error.response.headers['x-description'])
+          })
+      })
+    },
     onSearch (value) {
       this.paramsFilter = {}
       this.searchQuery = value
diff --git a/ui/src/config/router.js b/ui/src/config/router.js
index 90fae577ce4..0d0783a0906 100644
--- a/ui/src/config/router.js
+++ b/ui/src/config/router.js
@@ -84,7 +84,8 @@ function generateRouterMap (section) {
           searchFilters: child.searchFilters,
           related: child.related,
           actions: child.actions,
-          tabs: child.tabs
+          tabs: child.tabs,
+          customParamHandler: child.customParamHandler
         },
         component: component,
         hideChildrenInMenu: true,
diff --git a/ui/src/config/section/plugin/quota.js 
b/ui/src/config/section/plugin/quota.js
index ffa1ae86e65..630e42e4c04 100644
--- a/ui/src/config/section/plugin/quota.js
+++ b/ui/src/config/section/plugin/quota.js
@@ -16,6 +16,8 @@
 // under the License.
 
 import { shallowRef, defineAsyncComponent } from 'vue'
+import { i18n } from '@/locales'
+
 export default {
   name: 'quota',
   title: 'label.quota',
@@ -78,9 +80,102 @@ export default {
       icon: 'credit-card-outlined',
       docHelp: 'plugins/quota.html#quota-tariff',
       permission: ['quotaTariffList'],
-      columns: ['usageName', 'usageTypeDescription', 'usageUnit', 
'tariffValue', 'tariffActions'],
-      details: ['usageName', 'usageTypeDescription', 'usageUnit', 
'tariffValue'],
-      component: shallowRef(() => 
import('@/views/plugins/quota/QuotaTariff.vue'))
+      customParamHandler: (params, query) => {
+        params.listall = false
+
+        if (['all', 'removed'].includes(query.filter) || params.id) {
+          params.listall = true
+        }
+
+        if (['removed'].includes(query.filter)) {
+          params.listonlyremoved = true
+        }
+
+        return params
+      },
+      columns: [
+        'name',
+        {
+          field: 'usageName',
+          customTitle: 'usageType',
+          usageName: (record) => i18n.global.t(record.usageName)
+        },
+        {
+          field: 'usageUnit',
+          customTitle: 'usageUnit',
+          usageUnit: (record) => i18n.global.t(record.usageUnit)
+        },
+        {
+          field: 'tariffValue',
+          customTitle: 'quota.tariff.value'
+        },
+        {
+          field: 'executionPosition',
+          customTitle: 'quota.tariff.position',
+          executionPosition: (record) => record.position
+        },
+        {
+          field: 'effectiveDate',
+          customTitle: 'start.date'
+        },
+        {
+          field: 'endDate',
+          customTitle: 'end.date'
+        },
+        'removed'
+      ],
+      details: [
+        'uuid',
+        'name',
+        'description',
+        {
+          field: 'usageName',
+          customTitle: 'usageType'
+        },
+        'usageUnit',
+        {
+          field: 'tariffValue',
+          customTitle: 'quota.tariff.value'
+        },
+        {
+          field: 'effectiveDate',
+          customTitle: 'start.date'
+        },
+        {
+          field: 'endDate',
+          customTitle: 'end.date'
+        },
+        'removed'
+      ],
+      filters: ['all', 'active', 'removed'],
+      searchFilters: ['usagetype'],
+      actions: [
+        {
+          api: 'quotaTariffCreate',
+          icon: 'plus-outlined',
+          label: 'label.action.quota.tariff.create',
+          listView: true,
+          popup: true,
+          component: shallowRef(defineAsyncComponent(() => 
import('@/views/plugins/quota/CreateQuotaTariff.vue')))
+        },
+        {
+          api: 'quotaTariffUpdate',
+          icon: 'edit-outlined',
+          label: 'label.action.quota.tariff.edit',
+          dataView: true,
+          popup: true,
+          show: (record) => !record.removed,
+          component: shallowRef(defineAsyncComponent(() => 
import('@/views/plugins/quota/EditQuotaTariff.vue')))
+        },
+        {
+          api: 'quotaTariffDelete',
+          icon: 'delete-outlined',
+          label: 'label.action.quota.tariff.remove',
+          message: 'message.action.quota.tariff.remove',
+          dataView: true,
+          show: (record) => !record.removed
+        }
+      ]
     },
     {
       name: 'quotaemailtemplate',
diff --git a/ui/src/style/objects/form.scss b/ui/src/style/objects/form.scss
new file mode 100644
index 00000000000..ba56694ed38
--- /dev/null
+++ b/ui/src/style/objects/form.scss
@@ -0,0 +1,28 @@
+// 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.
+
+.form {
+  width: 80vw;
+
+  .full-width-input {
+    width: 100%;
+  }
+
+  @media (min-width: 500px) {
+    width: 400px;
+  }
+}
diff --git a/ui/src/utils/date.js b/ui/src/utils/date.js
new file mode 100644
index 00000000000..216dfde1303
--- /dev/null
+++ b/ui/src/utils/date.js
@@ -0,0 +1,104 @@
+// 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
+// the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+import store from '@/store'
+
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+
+dayjs.extend(utc)
+
+export function parseDayJsObject ({ value, format = true, keepMoment = true }) 
{
+  if (!value) {
+    return null
+  }
+
+  if (typeof value === 'string') {
+    value = dayjs(value)
+  }
+
+  if (!store.getters.usebrowsertimezone) {
+    value = value.utc(keepMoment)
+  }
+
+  if (!format) {
+    return value
+  }
+
+  return value.format()
+}
+
+/**
+ * When passing a string/dayjs to the date picker component, it converts the 
value to the browser timezone; therefore,
+ * we need to normalize the value to UTC if user is not using browser's 
timezone.
+ * @param {*} value The datetime to normalize.
+ * @returns A dayjs object with the datetime normalized to UTC if user is not 
using browser's timezone;
+ * otherwise, a correspondent dayjs object based on the value passed.
+ */
+export function parseDateToDatePicker (value) {
+  if (!value) {
+    return null
+  }
+
+  if (typeof value === 'string') {
+    value = dayjs(value)
+  }
+
+  if (store.getters.usebrowsertimezone) {
+    return value
+  }
+
+  return value.utc(false)
+}
+
+export function toLocalDate ({ date, timezoneoffset = 
store.getters.timezoneoffset, usebrowsertimezone = 
store.getters.usebrowsertimezone }) {
+  if (usebrowsertimezone) {
+    // Since GMT+530 is returned as -330 (minutes to GMT)
+    timezoneoffset = new Date().getTimezoneOffset() / -60
+  }
+
+  const milliseconds = Date.parse(date)
+  // e.g. "Tue, 08 Jun 2010 19:13:49 GMT"; "Tue, 25 May 2010 12:07:01 UTC"
+  return new Date(milliseconds + (timezoneoffset * 60 * 60 * 1000))
+}
+
+export function toLocaleDate ({ date, timezoneoffset = 
store.getters.timezoneoffset, usebrowsertimezone = 
store.getters.usebrowsertimezone, dateOnly = false, hourOnly = false }) {
+  if (!date) {
+    return null
+  }
+
+  let dateWithOffset = toLocalDate({ date, timezoneoffset, usebrowsertimezone 
}).toUTCString()
+
+  // e.g. "Mon, 03 Jun 2024 19:22:55 GMT" -> "03 Jun 2024 19:22:55 GMT"
+  dateWithOffset = dateWithOffset.substring(dateWithOffset.indexOf(', ') + 2)
+
+  // e.g. "03 Jun 2024 19:22:55 GMT" -> "03 Jun 2024 19:22:55"
+  dateWithOffset = dateWithOffset.substring(0, dateWithOffset.length - 4)
+
+  if (dateOnly) {
+    // e.g. "03 Jun 2024 19:22:55" -> "03 Jun 2024"
+    return dateWithOffset.substring(0, dateWithOffset.length - 9)
+  }
+
+  if (hourOnly) {
+    // e.g. "03 Jun 2024 19:22:55" -> "19:22:55"
+    return dateWithOffset.substring(dateWithOffset.length - 8, 
dateWithOffset.length)
+  }
+
+  return dateWithOffset
+}
+
+export { dayjs }
diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js
index d93c8796659..87ad11fbd46 100644
--- a/ui/src/utils/plugins.js
+++ b/ui/src/utils/plugins.js
@@ -22,6 +22,7 @@ import { message, notification } from 'ant-design-vue'
 import eventBus from '@/config/eventBus'
 import store from '@/store'
 import { sourceToken } from '@/utils/request'
+import { toLocalDate, toLocaleDate } from '@/utils/date'
 
 export const pollJobPlugin = {
   install (app) {
@@ -294,31 +295,13 @@ export const notifierPlugin = {
 export const toLocaleDatePlugin = {
   install (app) {
     app.config.globalProperties.$toLocaleDate = function (date) {
-      var timezoneOffset = this.$store.getters.timezoneoffset
-      if (this.$store.getters.usebrowsertimezone) {
-        // Since GMT+530 is returned as -330 (mins to GMT)
-        timezoneOffset = new Date().getTimezoneOffset() / -60
-      }
-      var milliseconds = Date.parse(date)
-      // e.g. "Tue, 08 Jun 2010 19:13:49 GMT", "Tue, 25 May 2010 12:07:01 UTC"
-      var dateWithOffset = new Date(milliseconds + (timezoneOffset * 60 * 60 * 
1000)).toUTCString()
-      // e.g. "08 Jun 2010 19:13:49 GMT", "25 May 2010 12:07:01 UTC"
-      dateWithOffset = dateWithOffset.substring(dateWithOffset.indexOf(', ') + 
2)
-      // e.g. "08 Jun 2010 19:13:49", "25 May 2010 12:10:16"
-      dateWithOffset = dateWithOffset.substring(0, dateWithOffset.length - 4)
-      return dateWithOffset
+      const { timezoneoffset, usebrowsertimezone } = this.$store.getters
+      return toLocaleDate({ date, timezoneoffset, usebrowsertimezone })
     }
 
     app.config.globalProperties.$toLocalDate = function (date) {
-      var timezoneOffset = this.$store.getters.timezoneoffset
-      if (this.$store.getters.usebrowsertimezone) {
-        // Since GMT+530 is returned as -330 (mins to GMT)
-        timezoneOffset = new Date().getTimezoneOffset() / -60
-      }
-      var milliseconds = Date.parse(date)
-      // e.g. "Tue, 08 Jun 2010 19:13:49 GMT", "Tue, 25 May 2010 12:07:01 UTC"
-      var dateWithOffset = new Date(milliseconds + (timezoneOffset * 60 * 60 * 
1000))
-      return dateWithOffset.toISOString()
+      const { timezoneoffset, usebrowsertimezone } = this.$store.getters
+      return toLocalDate({ date, timezoneoffset, usebrowsertimezone 
}).toISOString()
     }
   }
 }
diff --git a/ui/src/utils/quota.js b/ui/src/utils/quota.js
new file mode 100644
index 00000000000..485c99131d2
--- /dev/null
+++ b/ui/src/utils/quota.js
@@ -0,0 +1,124 @@
+// 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
+// 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.
+
+// Note: it could be retrieved from an API
+export const QUOTA_TYPES = [
+  {
+    id: 1,
+    type: 'RUNNING_VM'
+  },
+  {
+    id: 2,
+    type: 'ALLOCATED_VM'
+  },
+  {
+    id: 3,
+    type: 'IP_ADDRESS'
+  },
+  {
+    id: 4,
+    type: 'NETWORK_BYTES_SENT'
+  },
+  {
+    id: 5,
+    type: 'NETWORK_BYTES_RECEIVED'
+  },
+  {
+    id: 6,
+    type: 'VOLUME'
+  },
+  {
+    id: 7,
+    type: 'TEMPLATE'
+  },
+  {
+    id: 8,
+    type: 'ISO'
+  },
+  {
+    id: 9,
+    type: 'SNAPSHOT'
+  },
+  {
+    id: 10,
+    type: 'SECURITY_GROUP'
+  },
+  {
+    id: 11,
+    type: 'LOAD_BALANCER_POLICY'
+  },
+  {
+    id: 12,
+    type: 'PORT_FORWARDING_RULE'
+  },
+  {
+    id: 13,
+    type: 'NETWORK_OFFERING'
+  },
+  {
+    id: 14,
+    type: 'VPN_USERS'
+  },
+  {
+    id: 21,
+    type: 'VM_DISK_IO_READ'
+  },
+  {
+    id: 22,
+    type: 'VM_DISK_IO_WRITE'
+  },
+  {
+    id: 23,
+    type: 'VM_DISK_BYTES_READ'
+  },
+  {
+    id: 24,
+    type: 'VM_DISK_BYTES_WRITE'
+  },
+  {
+    id: 25,
+    type: 'VM_SNAPSHOT'
+  },
+  {
+    id: 26,
+    type: 'VOLUME_SECONDARY'
+  },
+  {
+    id: 27,
+    type: 'VM_SNAPSHOT_ON_PRIMARY'
+  },
+  {
+    id: 28,
+    type: 'BACKUP'
+  },
+  {
+    id: 29,
+    type: 'VPC'
+  },
+  {
+    id: 30,
+    type: 'NETWORK'
+  },
+  {
+    id: 31,
+    type: 'BACKUP_OBJECT'
+  }
+]
+
+export const getQuotaTypes = () => {
+  return QUOTA_TYPES.sort((a, b) => a.type.localeCompare(b.type))
+}
diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue
index 36eb6d4de1d..b0ffdb242ce 100644
--- a/ui/src/views/AutogenView.vue
+++ b/ui/src/views/AutogenView.vue
@@ -418,7 +418,7 @@
           @update-selected-columns="updateSelectedColumns"
           @selection-change="onRowSelectionChange"
           @refresh="fetchData"
-          @edit-tariff-action="(showAction, record) => 
$emit('edit-tariff-action', showAction, record)"/>
+        />
         <a-pagination
           class="row-element"
           style="margin-top: 10px"
@@ -694,7 +694,7 @@ export default {
       if (['volume'].includes(routeName)) {
         return 'user'
       }
-      if (['event', 'computeoffering', 'systemoffering', 
'diskoffering'].includes(routeName)) {
+      if (['event', 'computeoffering', 'systemoffering', 'diskoffering', 
'quotatariff'].includes(routeName)) {
         return 'active'
       }
       return 'self'
@@ -955,6 +955,11 @@ export default {
         params.showIcon = true
       }
 
+      const customParamHandler = this.$route.meta.customParamHandler
+      if (customParamHandler && typeof customParamHandler === 'function') {
+        params = customParamHandler(params, this.$route.query)
+      }
+
       if (['listAnnotations', 'listRoles', 'listZonesMetrics', 'listPods',
         'listClustersMetrics', 'listHostsMetrics', 'listStoragePoolsMetrics',
         'listImageStores', 'listSystemVms', 'listManagementServers',
diff --git a/ui/src/views/infra/UsageRecords.vue 
b/ui/src/views/infra/UsageRecords.vue
index 3ecfff5dc3b..735d91c5b8a 100644
--- a/ui/src/views/infra/UsageRecords.vue
+++ b/ui/src/views/infra/UsageRecords.vue
@@ -641,7 +641,7 @@ export default {
         if (json && json.listusagetypesresponse && 
json.listusagetypesresponse.usagetype) {
           this.usageTypes = [{ id: null, value: '' }, 
...json.listusagetypesresponse.usagetype.map(x => {
             return {
-              id: x.usagetypeid,
+              id: x.id,
               value: x.description
             }
           })]
diff --git a/ui/src/views/plugins/quota/CreateQuotaTariff.vue 
b/ui/src/views/plugins/quota/CreateQuotaTariff.vue
new file mode 100644
index 00000000000..bf8cb743b41
--- /dev/null
+++ b/ui/src/views/plugins/quota/CreateQuotaTariff.vue
@@ -0,0 +1,201 @@
+// 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.
+
+<template>
+  <a-spin :spinning="loading">
+    <a-form
+      class="form"
+      layout="vertical"
+      :ref="formRef"
+      :model="form"
+      :rules="rules"
+      @finish="handleSubmit"
+      v-ctrl-enter="handleSubmit">
+      <a-form-item ref="name" name="name">
+        <template #label>
+          <tooltip-label :title="$t('label.name')" 
:tooltip="apiParams.name.description"/>
+        </template>
+        <a-input
+          v-focus="true"
+          v-model:value="form.name"
+          :placeholder="$t('placeholder.quota.tariff.name')"
+          :max-length="65535"/>
+      </a-form-item>
+      <a-form-item ref="description" name="description">
+        <template #label>
+          <tooltip-label :title="$t('label.description')" 
:tooltip="apiParams.description.description"/>
+        </template>
+        <a-textarea
+          v-model:value="form.description"
+          :placeholder="$t('placeholder.quota.tariff.description')"
+          :max-length="65535" />
+      </a-form-item>
+      <a-form-item ref="usageType" name="usageType">
+        <template #label>
+          <tooltip-label :title="$t('label.quota.type.name')" 
:tooltip="apiParams.usagetype.description"/>
+        </template>
+        <a-select
+          v-model:value="form.usageType"
+          show-search
+          :placeholder="$t('placeholder.quota.tariff.usagetype')">
+          <a-select-option v-for="quotaType of getQuotaTypes()" 
:value="`${quotaType.id}-${quotaType.type}`" :key="quotaType.id">
+            {{ $t(quotaType.type) }}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item ref="value" name="value">
+        <template #label>
+          <tooltip-label :title="$t('label.quota.tariff.value')" 
:tooltip="apiParams.value.description"/>
+        </template>
+        <a-input-number
+          class="full-width-input"
+          v-model:value="form.value"
+          :placeholder="$t('placeholder.quota.tariff.value')" />
+      </a-form-item>
+      <a-form-item ref="position" name="position">
+        <template #label>
+          <tooltip-label :title="$t('label.quota.tariff.position')" 
:tooltip="apiParams.position.description" />
+        </template>
+        <a-input-number
+          class="full-width-input"
+          v-model:value="form.position"
+          :placeholder="$t('placeholder.quota.tariff.position')" />
+      </a-form-item>
+      <a-form-item ref="startDate" name="startDate">
+        <template #label>
+          <tooltip-label :title="$t('label.start.date')" 
:tooltip="apiParams.startdate.description"/>
+        </template>
+        <a-date-picker
+          class="full-width-input"
+          v-model:value="form.startDate"
+          :disabled-date="disabledStartDate"
+          :placeholder="$t('placeholder.quota.tariff.startdate')"
+          show-time
+        />
+      </a-form-item>
+      <a-form-item ref="endDate" name="endDate">
+        <template #label>
+          <tooltip-label :title="$t('label.end.date')" 
:tooltip="apiParams.enddate.description"/>
+        </template>
+        <a-date-picker
+          class="full-width-input"
+          v-model:value="form.endDate"
+          :disabled-date="disabledEndDate"
+          :placeholder="$t('placeholder.quota.tariff.enddate')"
+          show-time
+        />
+      </a-form-item>
+      <div :span="24" class="action-button">
+        <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+        <a-button type="primary" ref="submit" @click="handleSubmit">{{ 
$t('label.ok') }}</a-button>
+      </div>
+    </a-form>
+  </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+import { ref, reactive, toRaw } from 'vue'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+import { getQuotaTypes } from '@/utils/quota'
+import { dayjs, parseDayJsObject } from '@/utils/date'
+import { mixinForm } from '@/utils/mixin'
+
+export default {
+  name: 'CreateQuotaTariff',
+  mixins: [mixinForm],
+  components: {
+    TooltipLabel
+  },
+  data () {
+    return {
+      loading: false,
+      dayjs
+    }
+  },
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('quotaTariffCreate')
+  },
+  created () {
+    this.initForm()
+  },
+  inject: ['parentFetchData'],
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({
+        value: 0,
+        processingPeriod: 'BY_ENTRY'
+      })
+      this.rules = reactive({
+        name: [{ required: true, message: 
this.$t('message.action.quota.tariff.create.error.namerequired') }],
+        usageType: [{ required: true, message: 
this.$t('message.action.quota.tariff.create.error.usagetyperequired') }],
+        value: [{ required: true, message: 
this.$t('message.action.quota.tariff.create.error.valuerequired') }]
+      })
+      this.processingPeriod = 'BY_ENTRY'
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+
+      this.formRef.value.validate().then(() => {
+        const formRaw = toRaw(this.form)
+        const values = this.handleRemoveFields(formRaw)
+
+        values.usageType = values.usageType.split('-')[0]
+
+        if (values.startDate) {
+          values.startDate = parseDayJsObject({ value: values.startDate })
+        }
+
+        if (values.endDate) {
+          values.endDate = parseDayJsObject({ value: values.endDate })
+        }
+
+        this.loading = true
+        api('quotaTariffCreate', values).then(response => {
+          this.$message.success(this.$t('message.quota.tariff.create.success', 
{ quotaTariff: values.name }))
+          this.parentFetchData()
+          this.closeModal()
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.loading = false
+        })
+      }).catch((error) => {
+        this.formRef.value.scrollToField(error.errorFields[0].name)
+      })
+    },
+    closeModal () {
+      this.$emit('close-action')
+    },
+    getQuotaTypes () {
+      return getQuotaTypes()
+    },
+    disabledStartDate (current) {
+      return current < dayjs().startOf('day')
+    },
+    disabledEndDate (current) {
+      return current < (this.form.startDate || dayjs().startOf('day'))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/style/objects/form.scss';
+</style>
diff --git a/ui/src/views/plugins/quota/EditQuotaTariff.vue 
b/ui/src/views/plugins/quota/EditQuotaTariff.vue
new file mode 100644
index 00000000000..1a2bdb5cc13
--- /dev/null
+++ b/ui/src/views/plugins/quota/EditQuotaTariff.vue
@@ -0,0 +1,188 @@
+// 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.
+
+<template>
+  <a-spin :spinning="loading">
+    <a-form
+      class="form"
+      layout="vertical"
+      :ref="formRef"
+      :model="form"
+      @finish="handleSubmit"
+      v-ctrl-enter="handleSubmit">
+      <a-form-item ref="description" name="description">
+        <template #label>
+          <tooltip-label :title="$t('label.description')" 
:tooltip="apiParams.description.description"/>
+        </template>
+        <a-textarea
+          v-model:value="form.description"
+          :placeholder="$t('placeholder.quota.tariff.description')"
+          :max-length="65535" />
+      </a-form-item>
+      <a-form-item ref="value" name="value">
+        <template #label>
+          <tooltip-label :title="$t('label.quota.tariff.value')" 
:tooltip="apiParams.value.description"/>
+        </template>
+        <a-input-number
+          class="full-width-input"
+          v-model:value="form.value"
+          :placeholder="$t('placeholder.quota.tariff.value')" />
+      </a-form-item>
+      <a-form-item ref="position" name="position">
+        <template #label>
+         <tooltip-label :title="$t('label.quota.tariff.position')" 
:tooltip="apiParams.position.description"/>
+       </template>
+       <a-input-number
+          class="full-width-input"
+          v-model:value="form.position"
+          :placeholder="$t('placeholder.quota.tariff.position')" />
+      </a-form-item>
+      <a-form-item ref="endDate" name="endDate">
+        <template #label>
+          <tooltip-label :title="$t('label.end.date')" 
:tooltip="apiParams.enddate.description"/>
+        </template>
+        <a-date-picker
+          class="full-width-input"
+          v-model:value="form.endDate"
+          :disabled-date="disabledEndDate"
+          :placeholder="$t('placeholder.quota.tariff.enddate')"
+          show-time
+        />
+      </a-form-item>
+      <div :span="24" class="action-button">
+        <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+        <a-button type="primary" ref="submit" @click="handleSubmit">{{ 
$t('label.ok') }}</a-button>
+      </div>
+    </a-form>
+  </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+import { dayjs, parseDateToDatePicker, parseDayJsObject } from '@/utils/date'
+import { mixinForm } from '@/utils/mixin'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+import { ref, reactive, toRaw } from 'vue'
+import store from '@/store'
+
+export default {
+  name: 'EditQuotaTariff',
+  mixins: [mixinForm],
+  components: {
+    TooltipLabel
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  data: () => ({
+    loading: false,
+    dayjs
+  }),
+  inject: ['parentFetchData'],
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('quotaTariffUpdate')
+  },
+  created () {
+    this.initForm()
+  },
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({
+        description: this.resource.description,
+        value: this.resource.tariffValue,
+        position: this.resource.position,
+        endDate: parseDateToDatePicker(this.resource.endDate)
+      })
+    },
+    closeModal () {
+      this.$emit('close-action')
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+
+      this.formRef.value.validate().then(() => {
+        const formRaw = toRaw(this.form)
+        const values = this.handleRemoveFields(formRaw)
+
+        const params = {
+          name: this.resource.name
+        }
+
+        if (this.resource.description !== values.description) {
+          params.description = values.description
+        }
+
+        if (values.value && this.resource.tariffValue !== values.value) {
+          params.value = values.value
+        }
+
+        if (values.position && this.resource.position !== values.position) {
+          params.position = values.position
+        }
+
+        if (values.endDate && !values.endDate.isSame(this.resource.endDate)) {
+          params.enddate = parseDayJsObject({ value: values.endDate })
+        }
+
+        if (Object.keys(params).length === 1) {
+          this.closeModal()
+          return
+        }
+
+        this.loading = true
+
+        api('quotaTariffUpdate', {}, 'POST', params).then(json => {
+          const tariffResponse = json.quotatariffupdateresponse.quotatariff || 
{}
+          if (tariffResponse.id && this.$route.params.id) {
+            this.$router.push(`/quotatariff/${tariffResponse.id}`)
+          } else if (Object.keys(tariffResponse).length > 0) {
+            this.parentFetchData()
+          }
+
+          this.$message.success(this.$t('message.quota.tariff.update.success', 
{ quotaTariff: this.resource.name }))
+          this.closeModal()
+        }).catch(error => {
+          this.$notification.error({
+            message: this.$t('message.request.failed'),
+            description: (error.response && error.response.headers && 
error.response.headers['x-description']) || error.message
+          })
+        }).finally(() => {
+          this.loading = false
+        })
+      })
+    },
+    disabledEndDate (current) {
+      const lowerEndDateLimit = dayjs(this.resource.effectiveDate)
+      const startOfToday = dayjs().startOf('day')
+
+      if (store.getters.usebrowsertimezone) {
+        return current < startOfToday || current < 
lowerEndDateLimit.startOf('day')
+      }
+      return current < startOfToday || current < 
lowerEndDateLimit.utc(false).startOf('day')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/style/objects/form.scss';
+</style>
diff --git a/ui/src/views/plugins/quota/QuotaTariff.vue 
b/ui/src/views/plugins/quota/QuotaTariff.vue
deleted file mode 100644
index 0b3990a7563..00000000000
--- a/ui/src/views/plugins/quota/QuotaTariff.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-// 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.
-
-<template>
-  <div>
-    <autogen-view
-      ref="autogenview"
-      @edit-tariff-action="showTariffAction" />
-    <edit-tariff-value-wizard
-      v-if="tariffAction"
-      :showAction="tariffAction"
-      :resource="tariffResource"
-      @edit-tariff-action="showTariffAction"/>
-  </div>
-</template>
-
-<script>
-import AutogenView from '@/views/AutogenView.vue'
-import EditTariffValueWizard from '@/views/plugins/quota/EditTariffValueWizard'
-
-export default {
-  name: 'QuotaTariff',
-  components: {
-    AutogenView,
-    EditTariffValueWizard
-  },
-  data () {
-    return {
-      tariffAction: this.tariffAction,
-      tariffResource: this.tariffResource
-    }
-  },
-  provide: function () {
-    return {
-      parentFetchData: this.fetchData
-    }
-  },
-  methods: {
-    fetchData () {
-      this.$refs.autogenview.fetchData()
-    },
-    showTariffAction (showAction, resource) {
-      this.tariffAction = showAction
-      this.tariffResource = resource
-      this.loading = false
-    }
-  }
-}
-</script>
diff --git 
a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
 
b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
index 550e115d844..1a3d9d843c3 100644
--- 
a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
+++ 
b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java
@@ -53,6 +53,12 @@ public class JsInterpreter implements Closeable {
     private String timeoutDefaultMessage;
     protected Map<String, String> variables = new LinkedHashMap<>();
 
+    /**
+     * Constructor created exclusively for unit testing.
+     */
+    protected JsInterpreter() {
+    }
+
     public JsInterpreter(long timeout) {
         this.timeout = timeout;
         this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) 
defined in the global setting [quota.activationrule.timeout]: [%s].", 
this.timeout);
diff --git 
a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java
 
b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java
index a8c2e6ec0d3..ea8a0247f9c 100644
--- 
a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java
+++ 
b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java
@@ -42,11 +42,9 @@ import javax.script.ScriptEngine;
 
 @RunWith(MockitoJUnitRunner.class)
 public class JsInterpreterTest {
-    private long timeout = 2000;
-
     @InjectMocks
     @Spy
-    JsInterpreter jsInterpreterSpy = new JsInterpreter(timeout);
+    JsInterpreter jsInterpreterSpy = new JsInterpreter();
 
     @Mock
     ExecutorService executorMock;

Reply via email to