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;