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
This commit is contained in:
Bernardo De Marco Gonçalves 2024-08-15 14:16:44 -03:00 committed by GitHub
parent 3399abddb0
commit 01c721fcda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1176 additions and 312 deletions

View File

@ -696,6 +696,7 @@ public class ApiConstants {
public static final String TRAFFIC_TYPE_IMPLEMENTOR = "traffictypeimplementor"; public static final String TRAFFIC_TYPE_IMPLEMENTOR = "traffictypeimplementor";
public static final String KEYWORD = "keyword"; public static final String KEYWORD = "keyword";
public static final String LIST_ALL = "listall"; 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 LIST_SYSTEM_VMS = "listsystemvms";
public static final String IP_RANGES = "ipranges"; public static final String IP_RANGES = "ipranges";
public static final String IPV6_ROUTING = "ip6routing"; 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 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. * 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). * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0).

View File

@ -23,6 +23,7 @@ import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.UsageTypeResponse; import org.apache.cloudstack.api.response.UsageTypeResponse;
import org.apache.cloudstack.usage.UsageTypes;
import com.cloud.user.Account; import com.cloud.user.Account;
@ -37,8 +38,8 @@ public class ListUsageTypesCmd extends BaseCmd {
@Override @Override
public void execute() { public void execute() {
List<UsageTypeResponse> result = _usageService.listUsageTypes(); List<UsageTypeResponse> result = UsageTypes.listUsageTypes();
ListResponse<UsageTypeResponse> response = new ListResponse<UsageTypeResponse>(); ListResponse<UsageTypeResponse> response = new ListResponse<>();
response.setResponses(result); response.setResponses(result);
response.setResponseName(getCommandName()); response.setResponseName(getCommandName());
this.setResponseObject(response); this.setResponseObject(response);

View File

@ -25,12 +25,16 @@ import com.cloud.serializer.Param;
public class UsageTypeResponse extends BaseResponse { public class UsageTypeResponse extends BaseResponse {
@SerializedName("usagetypeid") @SerializedName("id")
@Param(description = "usage type") @Param(description = "Usage type ID")
private Integer usageType; private Integer usageType;
@SerializedName(ApiConstants.NAME)
@Param(description = "Usage type name")
private String name;
@SerializedName(ApiConstants.DESCRIPTION) @SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "description of usage type") @Param(description = "Usage type description")
private String description; private String description;
public String getDescription() { public String getDescription() {
@ -49,10 +53,10 @@ public class UsageTypeResponse extends BaseResponse {
this.usageType = usageType; this.usageType = usageType;
} }
public UsageTypeResponse(Integer usageType, String description) { public UsageTypeResponse(Integer usageType, String name, String description) {
this.usageType = usageType; this.usageType = usageType;
this.name = name;
this.description = description; this.description = description;
setObjectName("usagetype"); setObjectName("usagetype");
} }
} }

View File

@ -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.GenerateUsageRecordsCmd;
import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd; import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd;
import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd; import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd;
import org.apache.cloudstack.api.response.UsageTypeResponse;
import java.util.List; import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
@ -62,6 +61,4 @@ public interface UsageService {
TimeZone getUsageTimezone(); TimeZone getUsageTimezone();
boolean removeRawUsageRecords(RemoveRawUsageRecordsCmd cmd); boolean removeRawUsageRecords(RemoveRawUsageRecordsCmd cmd);
List<UsageTypeResponse> listUsageTypes();
} }

View File

@ -51,31 +51,31 @@ public class UsageTypes {
public static List<UsageTypeResponse> listUsageTypes() { public static List<UsageTypeResponse> listUsageTypes() {
List<UsageTypeResponse> responseList = new ArrayList<UsageTypeResponse>(); List<UsageTypeResponse> responseList = new ArrayList<UsageTypeResponse>();
responseList.add(new UsageTypeResponse(RUNNING_VM, "Running Vm Usage")); responseList.add(new UsageTypeResponse(RUNNING_VM, "RUNNING_VM", "Running Vm Usage"));
responseList.add(new UsageTypeResponse(ALLOCATED_VM, "Allocated Vm Usage")); responseList.add(new UsageTypeResponse(ALLOCATED_VM, "ALLOCATED_VM", "Allocated Vm Usage"));
responseList.add(new UsageTypeResponse(IP_ADDRESS, "IP Address Usage")); responseList.add(new UsageTypeResponse(IP_ADDRESS, "IP_ADDRESS", "IP Address Usage"));
responseList.add(new UsageTypeResponse(NETWORK_BYTES_SENT, "Network Usage (Bytes Sent)")); responseList.add(new UsageTypeResponse(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", "Network Usage (Bytes Sent)"));
responseList.add(new UsageTypeResponse(NETWORK_BYTES_RECEIVED, "Network Usage (Bytes Received)")); responseList.add(new UsageTypeResponse(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", "Network Usage (Bytes Received)"));
responseList.add(new UsageTypeResponse(VOLUME, "Volume Usage")); responseList.add(new UsageTypeResponse(VOLUME, "VOLUME", "Volume Usage"));
responseList.add(new UsageTypeResponse(TEMPLATE, "Template Usage")); responseList.add(new UsageTypeResponse(TEMPLATE, "TEMPLATE", "Template Usage"));
responseList.add(new UsageTypeResponse(ISO, "ISO Usage")); responseList.add(new UsageTypeResponse(ISO, "ISO", "ISO Usage"));
responseList.add(new UsageTypeResponse(SNAPSHOT, "Snapshot Usage")); responseList.add(new UsageTypeResponse(SNAPSHOT, "SNAPSHOT", "Snapshot Usage"));
responseList.add(new UsageTypeResponse(SECURITY_GROUP, "Security Group Usage")); responseList.add(new UsageTypeResponse(SECURITY_GROUP, "SECURITY_GROUP", "Security Group Usage"));
responseList.add(new UsageTypeResponse(LOAD_BALANCER_POLICY, "Load Balancer Usage")); responseList.add(new UsageTypeResponse(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", "Load Balancer Usage"));
responseList.add(new UsageTypeResponse(PORT_FORWARDING_RULE, "Port Forwarding Usage")); responseList.add(new UsageTypeResponse(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", "Port Forwarding Usage"));
responseList.add(new UsageTypeResponse(NETWORK_OFFERING, "Network Offering Usage")); responseList.add(new UsageTypeResponse(NETWORK_OFFERING, "NETWORK_OFFERING", "Network Offering Usage"));
responseList.add(new UsageTypeResponse(VPN_USERS, "VPN users usage")); responseList.add(new UsageTypeResponse(VPN_USERS, "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_READ, "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_IO_WRITE, "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_READ, "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_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", "VM Disk usage(Bytes Write)"));
responseList.add(new UsageTypeResponse(VM_SNAPSHOT, "VM Snapshot storage usage")); responseList.add(new UsageTypeResponse(VM_SNAPSHOT, "VM_SNAPSHOT", "VM Snapshot storage usage"));
responseList.add(new UsageTypeResponse(VOLUME_SECONDARY, "Volume on secondary 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 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 storage usage")); responseList.add(new UsageTypeResponse(BACKUP, "BACKUP", "Backup storage usage"));
responseList.add(new UsageTypeResponse(BUCKET, "Bucket storage usage")); responseList.add(new UsageTypeResponse(BUCKET, "BUCKET", "Bucket storage usage"));
responseList.add(new UsageTypeResponse(NETWORK, "Network usage")); responseList.add(new UsageTypeResponse(NETWORK, "NETWORK", "Network usage"));
responseList.add(new UsageTypeResponse(VPC, "VPC usage")); responseList.add(new UsageTypeResponse(VPC, "VPC", "VPC usage"));
return responseList; return responseList;
} }
} }

View File

@ -28,18 +28,10 @@ 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); Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, Date endDate, Integer usageType, String name, String uuid, boolean listAll, Long startIndex, Long pageSize);
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);
QuotaTariffVO findByName(String name); QuotaTariffVO findByName(String name);
QuotaTariffVO findTariffPlanByUsageType(int quotaType, Date onOrBefore);
Pair<List<QuotaTariffVO>, Integer> listAllTariffPlans();
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);
Boolean updateQuotaTariff(QuotaTariffVO plan); Boolean updateQuotaTariff(QuotaTariffVO plan);
QuotaTariffVO addQuotaTariff(QuotaTariffVO plan); QuotaTariffVO addQuotaTariff(QuotaTariffVO plan);

View File

@ -16,12 +16,9 @@
//under the License. //under the License.
package org.apache.cloudstack.quota.dao; package org.apache.cloudstack.quota.dao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.cloudstack.quota.constant.QuotaTypes;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component; 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.Transaction;
import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.TransactionStatus;
@Component @Component
public class QuotaTariffDaoImpl extends GenericDaoBase<QuotaTariffVO, Long> implements QuotaTariffDao { public class QuotaTariffDaoImpl extends GenericDaoBase<QuotaTariffVO, Long> implements QuotaTariffDao {
@ -45,7 +41,7 @@ public class QuotaTariffDaoImpl extends GenericDaoBase<QuotaTariffVO, Long> impl
public QuotaTariffDaoImpl() { public QuotaTariffDaoImpl() {
super(); super();
searchUsageType = createSearchBuilder(); searchUsageType = createSearchBuilder();
searchUsageType.and("usage_type", searchUsageType.entity().getUsageType(), SearchCriteria.Op.EQ); searchUsageType.and("usageType", searchUsageType.entity().getUsageType(), SearchCriteria.Op.EQ);
searchUsageType.done(); searchUsageType.done();
listAllIncludedUsageType = createSearchBuilder(); listAllIncludedUsageType = createSearchBuilder();
@ -54,111 +50,28 @@ public class QuotaTariffDaoImpl extends GenericDaoBase<QuotaTariffVO, Long> impl
listAllIncludedUsageType.done(); 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 @Override
public Boolean updateQuotaTariff(final QuotaTariffVO plan) { public Boolean updateQuotaTariff(final QuotaTariffVO plan) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<Boolean>() { return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<Boolean>) status -> update(plan.getId(), plan));
@Override
public Boolean doInTransaction(final TransactionStatus status) {
return update(plan.getId(), plan);
}
});
} }
@Override @Override
public QuotaTariffVO addQuotaTariff(final QuotaTariffVO plan) { public QuotaTariffVO addQuotaTariff(final QuotaTariffVO plan) {
if (plan.getIdObj() != null) { 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>() { return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<QuotaTariffVO>) status -> persist(plan));
@Override
public QuotaTariffVO doInTransaction(final TransactionStatus status) {
return persist(plan);
}
});
} }
@Override @Override
public Pair<List<QuotaTariffVO>, Integer> listQuotaTariffs(Date startDate, Date endDate, Integer usageType, String name, String uuid, boolean listAll, Long startIndex, Long pageSize) { 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); Filter sorter = new Filter(QuotaTariffVO.class, "usageType", false, startIndex, pageSize);
sorter.addOrderBy(QuotaTariffVO.class, "effectiveOn", false); sorter.addOrderBy(QuotaTariffVO.class, "effectiveOn", false);
sorter.addOrderBy(QuotaTariffVO.class, "updatedOn", 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)); 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) { protected SearchCriteria<QuotaTariffVO> createListQuotaTariffsSearchCriteria(Date startDate, Date endDate, Integer usageType, String name, String uuid, boolean listOnlyRemoved, String keyword) {
SearchCriteria<QuotaTariffVO> searchCriteria = createListQuotaTariffsSearchBuilder(startDate, endDate, usageType, name, uuid).create(); SearchCriteria<QuotaTariffVO> searchCriteria = createListQuotaTariffsSearchBuilder(listOnlyRemoved).create();
searchCriteria.setParametersIfNotNull("start_date", startDate); searchCriteria.setParametersIfNotNull("startDate", startDate);
searchCriteria.setParametersIfNotNull("end_date", endDate); searchCriteria.setParametersIfNotNull("endDate", endDate);
searchCriteria.setParametersIfNotNull("usage_type", usageType); searchCriteria.setParametersIfNotNull("usageType", usageType);
searchCriteria.setParametersIfNotNull("name", name); searchCriteria.setParametersIfNotNull("name", name);
searchCriteria.setParametersIfNotNull("uuid", uuid); searchCriteria.setParametersIfNotNull("uuid", uuid);
if (keyword != null) {
searchCriteria.setParameters("nameLike", "%" + keyword + "%");
}
return searchCriteria; 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(); SearchBuilder<QuotaTariffVO> searchBuilder = createSearchBuilder();
if (startDate != null) { searchBuilder.and("startDate", searchBuilder.entity().getEffectiveOn(), SearchCriteria.Op.GTEQ);
searchBuilder.and("start_date", 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 (endDate != null) { if (listOnlyRemoved) {
searchBuilder.and("end_date", searchBuilder.entity().getEndDate(), SearchCriteria.Op.LTEQ); searchBuilder.and("removed", searchBuilder.entity().getRemoved(), SearchCriteria.Op.NNULL);
}
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);
}
if (uuid != null) {
searchBuilder.and("uuid", searchBuilder.entity().getUuid(), SearchCriteria.Op.EQ);
} }
return searchBuilder; return searchBuilder;

View File

@ -62,5 +62,10 @@
<artifactId>joda-time</artifactId> <artifactId>joda-time</artifactId>
<version>${cs.joda-time.version}</version> <version>${cs.joda-time.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-api-discovery</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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.") @Parameter(name = "value", type = CommandType.DOUBLE, required = true, description = "The quota tariff value of the resource as per the default unit.")
private Double value; 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 " + @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, description = ApiConstants.PARAMETER_DESCRIPTION_ACTIVATION_RULE, length = 65535)
"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)
private String activationRule; 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 " + @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."); 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()); response.setResponseName(getCommandName());
setResponseObject(response); setResponseObject(response);
} }

View File

@ -17,15 +17,18 @@
package org.apache.cloudstack.api.command; package org.apache.cloudstack.api.command;
import com.cloud.user.Account; import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.Pair; import com.cloud.utils.Pair;
import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.api.response.QuotaTariffResponse; import org.apache.cloudstack.api.response.QuotaTariffResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; 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") + "list all, including the removed ones. The default is false.", since = "4.18.0.0")
private boolean listAll = false; private boolean listAll = false;
public QuotaTariffListCmd() { @Parameter(name = ApiConstants.LIST_ONLY_REMOVED, type = CommandType.BOOLEAN, description = "If set to true, we will list only the removed tariffs."
super(); + " 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 @Override
public void execute() { public void execute() {
final Pair<List<QuotaTariffVO>, Integer> result = _responseBuilder.listQuotaTariffPlans(this); 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<>(); 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()) { for (final QuotaTariffVO resource : result.first()) {
responses.add(_responseBuilder.createQuotaTariffResponse(resource)); responses.add(_responseBuilder.createQuotaTariffResponse(resource, returnActivationRules));
} }
final ListResponse<QuotaTariffResponse> response = new ListResponse<>(); final ListResponse<QuotaTariffResponse> response = new ListResponse<>();
@ -106,4 +118,15 @@ public class QuotaTariffListCmd extends BaseListCmd {
return listAll; return listAll;
} }
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isListOnlyRemoved() {
return listOnlyRemoved;
}
} }

View File

@ -63,10 +63,8 @@ public class QuotaTariffUpdateCmd extends BaseCmd {
since = "4.18.0.0") since = "4.18.0.0")
private String description; 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 " + @Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, description = ApiConstants.PARAMETER_DESCRIPTION_ACTIVATION_RULE +
"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 " + " Inform empty to remove the activation rule.", length = 65535, since = "4.18.0.0")
"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")
private String activationRule; 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") @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) { if (result == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update quota tariff plan"); 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()); response.setResponseName(getCommandName());
setResponseObject(response); setResponseObject(response);
} }

View File

@ -16,6 +16,7 @@
//under the License. //under the License.
package org.apache.cloudstack.api.response; package org.apache.cloudstack.api.response;
import com.cloud.user.User;
import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
@ -41,7 +42,9 @@ public interface QuotaResponseBuilder {
Pair<List<QuotaTariffVO>, Integer> listQuotaTariffPlans(QuotaTariffListCmd cmd); 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); QuotaStatementResponse createQuotaStatementResponse(List<QuotaUsageVO> quotaUsage);

View File

@ -52,6 +52,7 @@ import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd;
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.discovery.ApiDiscoveryService;
import org.apache.cloudstack.quota.QuotaManager; import org.apache.cloudstack.quota.QuotaManager;
import org.apache.cloudstack.quota.QuotaManagerImpl; import org.apache.cloudstack.quota.QuotaManagerImpl;
import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaService;
@ -135,8 +136,11 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
private final Class<?>[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; private final Class<?>[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class};
@Inject
private ApiDiscoveryService apiDiscoveryService;
@Override @Override
public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff) { public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) {
final QuotaTariffResponse response = new QuotaTariffResponse(); final QuotaTariffResponse response = new QuotaTariffResponse();
response.setUsageType(tariff.getUsageType()); response.setUsageType(tariff.getUsageType());
response.setUsageName(tariff.getUsageName()); response.setUsageName(tariff.getUsageName());
@ -146,13 +150,15 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
response.setEffectiveOn(tariff.getEffectiveOn()); response.setEffectiveOn(tariff.getEffectiveOn());
response.setUsageTypeDescription(tariff.getUsageTypeDescription()); response.setUsageTypeDescription(tariff.getUsageTypeDescription());
response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
response.setActivationRule(tariff.getActivationRule());
response.setName(tariff.getName()); response.setName(tariff.getName());
response.setEndDate(tariff.getEndDate()); response.setEndDate(tariff.getEndDate());
response.setDescription(tariff.getDescription()); response.setDescription(tariff.getDescription());
response.setId(tariff.getUuid()); response.setId(tariff.getUuid());
response.setRemoved(tariff.getRemoved()); response.setRemoved(tariff.getRemoved());
response.setPosition(tariff.getPosition()); response.setPosition(tariff.getPosition());
if (returnActivationRule) {
response.setActivationRule(tariff.getActivationRule());
}
return response; 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 @Override
public QuotaBalanceResponse createQuotaBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate, Date endDate) { public QuotaBalanceResponse createQuotaBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate, Date endDate) {
if (quotaBalance == null || quotaBalance.isEmpty()) { if (quotaBalance == null || quotaBalance.isEmpty()) {
@ -400,11 +411,14 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
boolean listAll = cmd.isListAll(); boolean listAll = cmd.isListAll();
Long startIndex = cmd.getStartIndex(); Long startIndex = cmd.getStartIndex();
Long pageSize = cmd.getPageSizeVal(); 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", logger.debug("Listing quota tariffs for parameters [{}].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "effectiveDate",
"endDate", "listAll", "name", "page", "pageSize", "usageType"))); "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 @Override

View File

@ -16,15 +16,18 @@
// under the License. // under the License.
package org.apache.cloudstack.api.command; package org.apache.cloudstack.api.command;
import com.cloud.user.User;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.api.response.QuotaTariffResponse; 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.constant.QuotaTypes;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@ -40,6 +43,12 @@ public class QuotaTariffListCmdTest extends TestCase {
@Mock @Mock
QuotaResponseBuilder responseBuilder; QuotaResponseBuilder responseBuilder;
@Mock
User userMock;
@Mock
CallContext callContextMock;
@Test @Test
public void testQuotaTariffListCmd() throws NoSuchFieldException, IllegalAccessException { public void testQuotaTariffListCmd() throws NoSuchFieldException, IllegalAccessException {
QuotaTariffListCmd cmd = new QuotaTariffListCmd(); QuotaTariffListCmd cmd = new QuotaTariffListCmd();
@ -48,17 +57,24 @@ public class QuotaTariffListCmdTest extends TestCase {
rbField.setAccessible(true); rbField.setAccessible(true);
rbField.set(cmd, responseBuilder); rbField.set(cmd, responseBuilder);
List<QuotaTariffVO> quotaTariffVOList = new ArrayList<QuotaTariffVO>(); List<QuotaTariffVO> quotaTariffVOList = new ArrayList<>();
QuotaTariffVO tariff = new QuotaTariffVO(); QuotaTariffVO tariff = new QuotaTariffVO();
tariff.setEffectiveOn(new Date()); tariff.setEffectiveOn(new Date());
tariff.setCurrencyValue(new BigDecimal(100)); tariff.setCurrencyValue(new BigDecimal(100));
tariff.setUsageType(QuotaTypes.VOLUME); tariff.setUsageType(QuotaTypes.VOLUME);
quotaTariffVOList.add(new QuotaTariffVO()); 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(); try (MockedStatic<CallContext> callContextStaticMock = Mockito.mockStatic(CallContext.class)) {
Mockito.verify(responseBuilder, Mockito.times(1)).createQuotaTariffResponse(Mockito.any(QuotaTariffVO.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));
} }
} }

View File

@ -60,8 +60,8 @@ public class QuotaTariffUpdateCmdTest extends TestCase {
} }
Mockito.when(responseBuilder.updateQuotaTariffPlan(Mockito.eq(cmd))).thenReturn(tariff); 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(); 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));
} }
} }

View File

@ -55,7 +55,10 @@ import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.cloudstack.discovery.ApiDiscoveryService;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -69,6 +72,7 @@ import com.cloud.user.Account;
import com.cloud.user.AccountVO; import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.UserDao; import com.cloud.user.dao.UserDao;
import com.cloud.user.User;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
@ -91,6 +95,12 @@ public class QuotaResponseBuilderImplTest extends TestCase {
@Mock @Mock
UserDao userDaoMock; UserDao userDaoMock;
@Mock
User userMock;
@Mock
ApiDiscoveryService discoveryServiceMock;
@Mock @Mock
QuotaService quotaServiceMock; QuotaService quotaServiceMock;
@ -164,11 +174,29 @@ public class QuotaResponseBuilderImplTest extends TestCase {
@Test @Test
public void testQuotaResponse() { public void testQuotaResponse() {
QuotaTariffVO tariffVO = makeTariffTestData(); QuotaTariffVO tariffVO = makeTariffTestData();
QuotaTariffResponse response = quotaResponseBuilderSpy.createQuotaTariffResponse(tariffVO); QuotaTariffResponse response = quotaResponseBuilderSpy.createQuotaTariffResponse(tariffVO, true);
assertTrue(tariffVO.getUsageType() == response.getUsageType()); assertTrue(tariffVO.getUsageType() == response.getUsageType());
assertTrue(tariffVO.getCurrencyValue().equals(response.getTariffValue())); 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 @Test
public void testAddQuotaCredits() { public void testAddQuotaCredits() {
final long accountId = 2L; final long accountId = 2L;
@ -569,4 +597,52 @@ public class QuotaResponseBuilderImplTest extends TestCase {
Mockito.verify(quotaTariffVoMock).setPosition(position); 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));
}
} }

View File

@ -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.GenerateUsageRecordsCmd;
import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd; import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd;
import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd; 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.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.usage.Usage; import org.apache.cloudstack.usage.Usage;
@ -485,10 +484,4 @@ public class UsageServiceImpl extends ManagerBase implements UsageService, Manag
} }
return true; return true;
} }
@Override
public List<UsageTypeResponse> listUsageTypes() {
return UsageTypes.listUsageTypes();
}
} }

View File

@ -161,6 +161,9 @@
"label.action.patch.systemvm.processing": "Patching System VM....", "label.action.patch.systemvm.processing": "Patching System VM....",
"label.action.project.add.account": "Add Account to project", "label.action.project.add.account": "Add Account to project",
"label.action.project.add.user": "Add User 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.instance": "Reboot Instance",
"label.action.reboot.router": "Reboot router", "label.action.reboot.router": "Reboot router",
"label.action.reboot.systemvm": "Reboot System VM", "label.action.reboot.systemvm": "Reboot System VM",
@ -865,6 +868,7 @@
"label.encrypt": "Encrypt", "label.encrypt": "Encrypt",
"label.encryptroot": "Encrypt Root Disk", "label.encryptroot": "Encrypt Root Disk",
"label.end": "End", "label.end": "End",
"label.end.date": "End date",
"label.end.date.and.time": "End date and time", "label.end.date.and.time": "End date and time",
"label.end.ip": "End IP", "label.end.ip": "End IP",
"label.end.reserved.system.ip": "End reserved system IP", "label.end.reserved.system.ip": "End reserved system IP",
@ -1727,6 +1731,8 @@
"label.quota.summary": "Summary", "label.quota.summary": "Summary",
"label.quota.tariff": "Tariff", "label.quota.tariff": "Tariff",
"label.quota.tariff.effectivedate": "Effective date", "label.quota.tariff.effectivedate": "Effective date",
"label.quota.tariff.position": "Position",
"label.quota.tariff.value": "Tariff value",
"label.quota.total": "Total", "label.quota.total": "Total",
"label.quota.type.name": "Usage Type", "label.quota.type.name": "Usage Type",
"label.quota.type.unit": "Usage unit", "label.quota.type.unit": "Usage unit",
@ -2060,6 +2066,7 @@
"label.sslverification": "SSL verification", "label.sslverification": "SSL verification",
"label.standard.us.keyboard": "Standard (US) keyboard", "label.standard.us.keyboard": "Standard (US) keyboard",
"label.start": "Start", "label.start": "Start",
"label.start.date": "Start date",
"label.start.date.and.time": "Start date and time", "label.start.date.and.time": "Start date and time",
"label.start.ip": "Start IP", "label.start.ip": "Start IP",
"label.start.lb.vm": "Start LB Instance", "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.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.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.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.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.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.", "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.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.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 acquire additional IPs to implement static NAT between their Instances and the public IP.", "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 acquire additional IPs to implement static NAT between their Instances and the public IP.",
"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.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.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.", "message.recover.vm": "Please confirm that you would like to recover this Instance.",
@ -3522,6 +3535,13 @@
"migrate.from": "Migrate from", "migrate.from": "Migrate from",
"migrate.to": "Migrate to", "migrate.to": "Migrate to",
"migrationPolicy": "Migration policy", "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", "router.health.checks": "Health check",
"side.by.side": "Side by Side", "side.by.side": "Side by Side",
"state.completed": "Completed", "state.completed": "Completed",
@ -3543,5 +3563,32 @@
"state.stopping": "Stopping", "state.stopping": "Stopping",
"state.suspended": "Suspended", "state.suspended": "Suspended",
"user.login": "Login", "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"
} }

View File

@ -162,6 +162,7 @@
"label.action.vmsnapshot.revert": "Reverter snapshot de VM", "label.action.vmsnapshot.revert": "Reverter snapshot de VM",
"label.action.vmstoragesnapshot.create": "Criar snapshot de volume da VM", "label.action.vmstoragesnapshot.create": "Criar snapshot de volume da VM",
"label.actions": "A\u00e7\u00f5es", "label.actions": "A\u00e7\u00f5es",
"label.active": "Ativo",
"label.activate.project": "Ativar projeto", "label.activate.project": "Ativar projeto",
"label.activeviewersessions": "Sess\u00f5es ativas", "label.activeviewersessions": "Sess\u00f5es ativas",
"label.add": "Adicionar", "label.add": "Adicionar",
@ -625,6 +626,7 @@
"label.enable.vpc.offering": "Habilitar oferta VPC", "label.enable.vpc.offering": "Habilitar oferta VPC",
"label.enable.vpn": "Habilitar VPN", "label.enable.vpn": "Habilitar VPN",
"label.end": "Fim", "label.end": "Fim",
"label.end.date": "Data de término",
"label.end.date.and.time": "Data e hor\u00e1rio final", "label.end.date.and.time": "Data e hor\u00e1rio final",
"label.end.ip": "IP final", "label.end.ip": "IP final",
"label.end.reserved.system.ip": "Fim dos IPs reservados para o sistema", "label.end.reserved.system.ip": "Fim dos IPs reservados para o sistema",
@ -1279,7 +1281,12 @@
"label.quotastate": "Estado da cota", "label.quotastate": "Estado da cota",
"label.summary": "Sum\u00e1rio", "label.summary": "Sum\u00e1rio",
"label.quota.tariff": "Tarifa", "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.effectivedate": "Data efetiva",
"label.quota.tariff.position": "Posi\u00e7\u00e3o",
"label.quota.tariff.value": "Valor",
"label.quota.total": "Total", "label.quota.total": "Total",
"label.quota.type.name": "Tipo de uso", "label.quota.type.name": "Tipo de uso",
"label.quota.type.unit": "Unidade do uso", "label.quota.type.unit": "Unidade do uso",
@ -1338,6 +1345,7 @@
"label.remove.vmware.datacenter": "Remover datacenter VMware", "label.remove.vmware.datacenter": "Remover datacenter VMware",
"label.remove.vpc": "Remover VPC", "label.remove.vpc": "Remover VPC",
"label.remove.vpc.offering": "Remover oferta VPC", "label.remove.vpc.offering": "Remover oferta VPC",
"label.removed": "Removido",
"label.removing": "Removendo", "label.removing": "Removendo",
"label.replace.acl": "Substituir ACL", "label.replace.acl": "Substituir ACL",
"label.replace.acl.list": "Substituir lista ACL", "label.replace.acl.list": "Substituir lista ACL",
@ -1518,6 +1526,7 @@
"label.standard.us.keyboard": "Teclado padr\u00e3o (EUA)", "label.standard.us.keyboard": "Teclado padr\u00e3o (EUA)",
"label.sslcertificates": "Certificados SSL", "label.sslcertificates": "Certificados SSL",
"label.start": "Iniciar", "label.start": "Iniciar",
"label.start.date": "Data de in\u00edcio",
"label.start.date.and.time": "Data e hor\u00e1rio inicial", "label.start.date.and.time": "Data e hor\u00e1rio inicial",
"label.start.ip": "IP do in\u00edcio", "label.start.ip": "IP do in\u00edcio",
"label.start.lb.vm": "Iniciar VM LB", "label.start.lb.vm": "Iniciar VM LB",
@ -1674,7 +1683,7 @@
"label.upload.volume.from.url": "Carregar volume por URL", "label.upload.volume.from.url": "Carregar volume por URL",
"label.url": "URL", "label.url": "URL",
"label.usageinterface": "Interface de uso", "label.usageinterface": "Interface de uso",
"label.usagename": "Tipo", "label.usagetype": "Tipo",
"label.usageunit": "Unidade", "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.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", "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.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.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.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.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.router": "Confirme que voc\ufffd deseja reiniciar este roteador.",
"message.action.reboot.systemvm": "Confirme que voc\u00ea deseja reiniciar esta VM de sistema.", "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.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.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 automaticamente entre o IP p\u00fablico e IP guest. Os usu\u00e1rios finais tamb\u00e9m podem usar a interface de usu\u00e1rio CloudStack para adquirir IPs adicionais afim de se implementar NAT est\u00e1tico entre suas inst\u00e2ncias e o IP p\u00fablico.", "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 automaticamente entre o IP p\u00fablico e IP guest. Os usu\u00e1rios finais tamb\u00e9m podem usar a interface de usu\u00e1rio CloudStack para adquirir IPs adicionais afim de se implementar NAT est\u00e1tico entre suas inst\u00e2ncias e o IP p\u00fablico.",
"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.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.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.", "message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.",
@ -2495,6 +2510,13 @@
"migrate.from": "Migrar de", "migrate.from": "Migrar de",
"migrate.to": "Migrar para", "migrate.to": "Migrar para",
"migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o", "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", "router.health.checks": "Checagem de sa\u00fade",
"side.by.side": "Lado a lado", "side.by.side": "Lado a lado",
"state.completed": "Completo", "state.completed": "Completo",
@ -2516,5 +2538,32 @@
"state.stopping": "Parando", "state.stopping": "Parando",
"state.suspended": "Suspendido", "state.suspended": "Suspendido",
"user.login": "Entrar", "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"
} }

View File

@ -38,8 +38,8 @@
:dataSource="fetchDetails()"> :dataSource="fetchDetails()">
<template #renderItem="{item}"> <template #renderItem="{item}">
<a-list-item v-if="(item in dataResource && !customDisplayItems.includes(item)) || (offeringDetails.includes(item) && dataResource.serviceofferingdetails)"> <a-list-item v-if="(item in dataResource && !customDisplayItems.includes(item)) || (offeringDetails.includes(item) && dataResource.serviceofferingdetails)">
<div> <div style="width: 100%">
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong> <strong>{{ item === 'service' ? $t('label.supportedservices') : $t(getDetailTitle(item)) }}</strong>
<br/> <br/>
<div v-if="Array.isArray(dataResource[item]) && item === 'service'"> <div v-if="Array.isArray(dataResource[item]) && item === 'service'">
<div v-for="(service, idx) in dataResource[item]" :key="idx"> <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-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> <span v-else>{{ dataResource[item] }}</span>
</div> </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]) }} {{ $toLocaleDate(dataResource[item]) }}
</div> </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 v-else-if="$route.meta.name === 'userdata' && item === 'userdata'">
<div style="white-space: pre-wrap;"> {{ decodeUserData(dataResource.userdata)}} </div> <div style="white-space: pre-wrap;"> {{ decodeUserData(dataResource.userdata)}} </div>
</div> </div>
@ -179,7 +180,8 @@ export default {
dedicatedRoutes: ['zone', 'pod', 'cluster', 'host'], dedicatedRoutes: ['zone', 'pod', 'cluster', 'host'],
dedicatedSectionActive: false, dedicatedSectionActive: false,
projectname: '', projectname: '',
dataResource: {} dataResource: {},
detailsTitles: []
} }
}, },
mounted () { mounted () {
@ -342,12 +344,33 @@ export default {
this.dataResource.account = projectAdmins.join() this.dataResource.account = projectAdmins.join()
}, },
fetchDetails () { fetchDetails () {
var details = this.$route.meta.details let details = this.$route.meta.details
if (!details) {
return
}
if (typeof details === 'function') { if (typeof details === 'function') {
details = details() 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()}`
} }
} }
} }

View File

@ -372,7 +372,7 @@
<status :text="record.enabled ? record.enabled.toString() : 'false'" /> <status :text="record.enabled ? record.enabled.toString() : 'false'" />
{{ record.enabled ? 'Enabled' : 'Disabled' }} {{ record.enabled ? 'Enabled' : 'Disabled' }}
</template> </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) }} {{ $toLocaleDate(text) }}
</template> </template>
<template v-if="['startdate', 'enddate'].includes(column.key) && ['vm', 'vnfapp'].includes($route.path.split('/')[1])"> <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', '/project', '/account', 'buckets', 'objectstore',
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation', '/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering', '/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) .test(this.$route.path)
}, },
enableGroupAction () { enableGroupAction () {
@ -970,7 +970,7 @@ export default {
col.width = w col.width = w
}, },
updateSelectedColumns (name) { updateSelectedColumns (name) {
this.$emit('update-selected-columns', name) this.$emit('update-selected-columns', this.getColumnKey(name))
}, },
getVmRouteUsingType (record) { getVmRouteUsingType (record) {
switch (record.virtualmachinetype) { switch (record.virtualmachinetype) {
@ -999,7 +999,7 @@ export default {
if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) { if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) {
this.usageTypes = json.listusagetypesresponse.usagetype.map(x => { this.usageTypes = json.listusagetypesresponse.usagetype.map(x => {
return { return {
id: x.usagetypeid, id: x.id,
value: x.description value: x.description
} }
}) })

View File

@ -162,6 +162,7 @@ import { isAdmin } from '@/role'
import TooltipButton from '@/components/widgets/TooltipButton' import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import Status from '@/components/widgets/Status' import Status from '@/components/widgets/Status'
import { i18n } from '@/locales'
export default { export default {
name: 'SearchView', name: 'SearchView',
@ -290,9 +291,13 @@ export default {
if (item === 'groupid' && !('listInstanceGroups' in this.$store.getters.apis)) { if (item === 'groupid' && !('listInstanceGroups' in this.$store.getters.apis)) {
return true return true
} }
if (item === 'usagetype' && !('listUsageTypes' in this.$store.getters.apis)) {
return true
}
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level', if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider', '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' type = 'list'
} else if (item === 'tags') { } else if (item === 'tags') {
@ -414,6 +419,7 @@ export default {
let managementServerIdIndex = -1 let managementServerIdIndex = -1
let serviceOfferingIndex = -1 let serviceOfferingIndex = -1
let diskOfferingIndex = -1 let diskOfferingIndex = -1
let usageTypeIndex = -1
if (arrayField.includes('type')) { if (arrayField.includes('type')) {
if (this.$route.path === '/alert') { if (this.$route.path === '/alert') {
@ -499,6 +505,12 @@ export default {
promises.push(await this.fetchDiskOfferings(searchKeyword)) 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 => { Promise.all(promises).then(response => {
if (typeIndex > -1) { if (typeIndex > -1) {
const types = response.filter(item => item.type === 'type') const types = response.filter(item => item.type === 'type')
@ -581,6 +593,12 @@ export default {
this.fields[diskOfferingIndex].opts = this.sortArray(diskOfferings[0].data) 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(() => { }).finally(() => {
if (typeIndex > -1) { if (typeIndex > -1) {
this.fields[typeIndex].loading = false this.fields[typeIndex].loading = false
@ -618,6 +636,9 @@ export default {
if (diskOfferingIndex > -1) { if (diskOfferingIndex > -1) {
this.fields[diskOfferingIndex].loading = false this.fields[diskOfferingIndex].loading = false
} }
if (usageTypeIndex > -1) {
this.fields[usageTypeIndex].loading = false
}
if (Array.isArray(arrayField)) { if (Array.isArray(arrayField)) {
this.fillFormFieldValues() this.fillFormFieldValues()
} }
@ -1165,6 +1186,27 @@ export default {
}) })
return levels 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) { onSearch (value) {
this.paramsFilter = {} this.paramsFilter = {}
this.searchQuery = value this.searchQuery = value

View File

@ -84,7 +84,8 @@ function generateRouterMap (section) {
searchFilters: child.searchFilters, searchFilters: child.searchFilters,
related: child.related, related: child.related,
actions: child.actions, actions: child.actions,
tabs: child.tabs tabs: child.tabs,
customParamHandler: child.customParamHandler
}, },
component: component, component: component,
hideChildrenInMenu: true, hideChildrenInMenu: true,

View File

@ -16,6 +16,8 @@
// under the License. // under the License.
import { shallowRef, defineAsyncComponent } from 'vue' import { shallowRef, defineAsyncComponent } from 'vue'
import { i18n } from '@/locales'
export default { export default {
name: 'quota', name: 'quota',
title: 'label.quota', title: 'label.quota',
@ -78,9 +80,102 @@ export default {
icon: 'credit-card-outlined', icon: 'credit-card-outlined',
docHelp: 'plugins/quota.html#quota-tariff', docHelp: 'plugins/quota.html#quota-tariff',
permission: ['quotaTariffList'], permission: ['quotaTariffList'],
columns: ['usageName', 'usageTypeDescription', 'usageUnit', 'tariffValue', 'tariffActions'], customParamHandler: (params, query) => {
details: ['usageName', 'usageTypeDescription', 'usageUnit', 'tariffValue'], params.listall = false
component: shallowRef(() => import('@/views/plugins/quota/QuotaTariff.vue'))
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', name: 'quotaemailtemplate',

View File

@ -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;
}
}

104
ui/src/utils/date.js Normal file
View File

@ -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 }

View File

@ -22,6 +22,7 @@ import { message, notification } from 'ant-design-vue'
import eventBus from '@/config/eventBus' import eventBus from '@/config/eventBus'
import store from '@/store' import store from '@/store'
import { sourceToken } from '@/utils/request' import { sourceToken } from '@/utils/request'
import { toLocalDate, toLocaleDate } from '@/utils/date'
export const pollJobPlugin = { export const pollJobPlugin = {
install (app) { install (app) {
@ -294,31 +295,13 @@ export const notifierPlugin = {
export const toLocaleDatePlugin = { export const toLocaleDatePlugin = {
install (app) { install (app) {
app.config.globalProperties.$toLocaleDate = function (date) { app.config.globalProperties.$toLocaleDate = function (date) {
var timezoneOffset = this.$store.getters.timezoneoffset const { timezoneoffset, usebrowsertimezone } = this.$store.getters
if (this.$store.getters.usebrowsertimezone) { return toLocaleDate({ date, timezoneoffset, 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
} }
app.config.globalProperties.$toLocalDate = function (date) { app.config.globalProperties.$toLocalDate = function (date) {
var timezoneOffset = this.$store.getters.timezoneoffset const { timezoneoffset, usebrowsertimezone } = this.$store.getters
if (this.$store.getters.usebrowsertimezone) { return toLocalDate({ date, timezoneoffset, usebrowsertimezone }).toISOString()
// 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()
} }
} }
} }

124
ui/src/utils/quota.js Normal file
View File

@ -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))
}

View File

@ -418,7 +418,7 @@
@update-selected-columns="updateSelectedColumns" @update-selected-columns="updateSelectedColumns"
@selection-change="onRowSelectionChange" @selection-change="onRowSelectionChange"
@refresh="fetchData" @refresh="fetchData"
@edit-tariff-action="(showAction, record) => $emit('edit-tariff-action', showAction, record)"/> />
<a-pagination <a-pagination
class="row-element" class="row-element"
style="margin-top: 10px" style="margin-top: 10px"
@ -694,7 +694,7 @@ export default {
if (['volume'].includes(routeName)) { if (['volume'].includes(routeName)) {
return 'user' return 'user'
} }
if (['event', 'computeoffering', 'systemoffering', 'diskoffering'].includes(routeName)) { if (['event', 'computeoffering', 'systemoffering', 'diskoffering', 'quotatariff'].includes(routeName)) {
return 'active' return 'active'
} }
return 'self' return 'self'
@ -955,6 +955,11 @@ export default {
params.showIcon = true 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', if (['listAnnotations', 'listRoles', 'listZonesMetrics', 'listPods',
'listClustersMetrics', 'listHostsMetrics', 'listStoragePoolsMetrics', 'listClustersMetrics', 'listHostsMetrics', 'listStoragePoolsMetrics',
'listImageStores', 'listSystemVms', 'listManagementServers', 'listImageStores', 'listSystemVms', 'listManagementServers',

View File

@ -641,7 +641,7 @@ export default {
if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) { if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) {
this.usageTypes = [{ id: null, value: '' }, ...json.listusagetypesresponse.usagetype.map(x => { this.usageTypes = [{ id: null, value: '' }, ...json.listusagetypesresponse.usagetype.map(x => {
return { return {
id: x.usagetypeid, id: x.id,
value: x.description value: x.description
} }
})] })]

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -53,6 +53,12 @@ public class JsInterpreter implements Closeable {
private String timeoutDefaultMessage; private String timeoutDefaultMessage;
protected Map<String, String> variables = new LinkedHashMap<>(); protected Map<String, String> variables = new LinkedHashMap<>();
/**
* Constructor created exclusively for unit testing.
*/
protected JsInterpreter() {
}
public JsInterpreter(long timeout) { public JsInterpreter(long timeout) {
this.timeout = timeout; this.timeout = timeout;
this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout); this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout);

View File

@ -42,11 +42,9 @@ import javax.script.ScriptEngine;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class JsInterpreterTest { public class JsInterpreterTest {
private long timeout = 2000;
@InjectMocks @InjectMocks
@Spy @Spy
JsInterpreter jsInterpreterSpy = new JsInterpreter(timeout); JsInterpreter jsInterpreterSpy = new JsInterpreter();
@Mock @Mock
ExecutorService executorMock; ExecutorService executorMock;