mirror of https://github.com/apache/cloudstack.git
Merge 14a9f8b8dc into b7d4df0a11
This commit is contained in:
commit
e7af8c505d
|
|
@ -286,6 +286,7 @@ public class ApiConstants {
|
|||
public static final String HEALTH = "health";
|
||||
public static final String HEADERS = "headers";
|
||||
public static final String HIDE_IP_ADDRESS_USAGE = "hideipaddressusage";
|
||||
public static final String HISTORY = "history";
|
||||
public static final String HOST_ID = "hostid";
|
||||
public static final String HOST_IDS = "hostids";
|
||||
public static final String HOST_IP = "hostip";
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import com.cloud.utils.db.GenericDao;
|
|||
|
||||
public interface QuotaCreditsDao extends GenericDao<QuotaCreditsVO, Long> {
|
||||
|
||||
List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive);
|
||||
List<QuotaCreditsVO> findCredits(Long accountId, List<Long> domainIds, Date startDate, Date endDate);
|
||||
|
||||
QuotaCreditsVO saveCredits(QuotaCreditsVO credits);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.util.List;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import com.cloud.domain.dao.DomainDao;
|
||||
import com.cloud.utils.db.Filter;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
|
||||
|
|
@ -39,8 +38,6 @@ import com.cloud.utils.db.TransactionStatus;
|
|||
@Component
|
||||
public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> implements QuotaCreditsDao {
|
||||
|
||||
@Inject
|
||||
DomainDao domainDao;
|
||||
@Inject
|
||||
QuotaBalanceDao quotaBalanceDao;
|
||||
|
||||
|
|
@ -50,19 +47,18 @@ public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> im
|
|||
quotaCreditsVoSearch = createSearchBuilder();
|
||||
quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN);
|
||||
quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
|
||||
quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN);
|
||||
quotaCreditsVoSearch.and("domainIds", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN);
|
||||
quotaCreditsVoSearch.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) {
|
||||
public List<QuotaCreditsVO> findCredits(Long accountId, List<Long> domainIds, Date startDate, Date endDate) {
|
||||
SearchCriteria<QuotaCreditsVO> sc = quotaCreditsVoSearch.create();
|
||||
Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
|
||||
|
||||
sc.setParametersIfNotNull("accountId", accountId);
|
||||
if (domainId != null) {
|
||||
List<Long> domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId);
|
||||
sc.setParameters("domainId", domainIds.toArray());
|
||||
if (domainIds != null) {
|
||||
sc.setParameters("domainIds", domainIds.toArray());
|
||||
}
|
||||
|
||||
if (ObjectUtils.allNotNull(startDate, endDate)) {
|
||||
|
|
|
|||
|
|
@ -21,14 +21,13 @@ import com.cloud.user.Account;
|
|||
import org.apache.cloudstack.api.ACL;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.AccountResponse;
|
||||
import org.apache.cloudstack.api.response.DomainResponse;
|
||||
import org.apache.cloudstack.api.response.ProjectResponse;
|
||||
import org.apache.cloudstack.api.response.QuotaCreditsResponse;
|
||||
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.quota.QuotaService;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -42,22 +41,35 @@ public class QuotaCreditsCmd extends BaseCmd {
|
|||
@Inject
|
||||
QuotaService _quotaService;
|
||||
|
||||
|
||||
|
||||
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which quota credits need to be added")
|
||||
@Deprecated
|
||||
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which Quota credits will be added. Deprecated, please use '" +
|
||||
ApiConstants.ACCOUNT_ID + "' instead.")
|
||||
private String accountName;
|
||||
|
||||
@ACL
|
||||
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Domain for which quota credits need to be added")
|
||||
@Deprecated
|
||||
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
|
||||
description = "Domain of the Account specified by '" + ApiConstants.ACCOUNT + "' for which Quota credits will be added. " +
|
||||
"Deprecated, please use '" + ApiConstants.ACCOUNT_ID + "' instead.")
|
||||
private Long domainId;
|
||||
|
||||
@Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Value of the credits to be added+, subtracted-")
|
||||
@ACL
|
||||
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
|
||||
description = "ID of the Account for which Quota credits will be added. Can not be specified with '" + ApiConstants.PROJECT_ID + "'.")
|
||||
private Long accountId;
|
||||
|
||||
@ACL
|
||||
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class,
|
||||
description = "ID of the Project for which Quota credits will be added. Can not be specified with '" + ApiConstants.ACCOUNT_ID + "'.")
|
||||
private Long projectId;
|
||||
|
||||
@Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Amount of credits to be added (in case of a positive value) or subtracted (in case of a negative value).")
|
||||
private Double value;
|
||||
|
||||
@Parameter(name = "min_balance", type = CommandType.DOUBLE, required = false, description = "Minimum balance threshold of the Account")
|
||||
@Parameter(name = "min_balance", type = CommandType.DOUBLE, description = "An email will be sent to the Account when the Quota credits get below this threshold.")
|
||||
private Double minBalance;
|
||||
|
||||
@Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, required = false, description = "Account for which quota enforce is set to false will not be locked when there is no credit balance")
|
||||
@Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, description = "Whether to lock the Account when Quota credits are below zero.")
|
||||
private Boolean quotaEnforce;
|
||||
|
||||
public Double getMinBalance() {
|
||||
|
|
@ -100,31 +112,21 @@ public class QuotaCreditsCmd extends BaseCmd {
|
|||
this.value = value;
|
||||
}
|
||||
|
||||
public Long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public Long getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public QuotaCreditsCmd() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
Long accountId = null;
|
||||
Account account = _accountService.getActiveAccountByName(accountName, domainId);
|
||||
if (account != null) {
|
||||
accountId = account.getAccountId();
|
||||
}
|
||||
if (accountId == null) {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "The Account does not exists or has been removed/disabled");
|
||||
}
|
||||
if (getValue() == null) {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please send a valid non-empty quota value");
|
||||
}
|
||||
if (getQuotaEnforce() != null) {
|
||||
_quotaService.setLockAccount(accountId, getQuotaEnforce());
|
||||
}
|
||||
if (getMinBalance() != null) {
|
||||
_quotaService.setMinBalance(accountId, getMinBalance());
|
||||
}
|
||||
|
||||
final QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(accountId, getDomainId(), getValue(), CallContext.current().getCallingUserId(), getQuotaEnforce());
|
||||
QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(this);
|
||||
response.setResponseName(getCommandName());
|
||||
response.setObjectName("quotacredits");
|
||||
setResponseObject(response);
|
||||
|
|
@ -132,10 +134,6 @@ public class QuotaCreditsCmd extends BaseCmd {
|
|||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
Account account = _accountService.getActiveAccountByName(accountName, domainId);
|
||||
if (account != null) {
|
||||
return account.getAccountId();
|
||||
}
|
||||
return Account.ACCOUNT_ID_SYSTEM;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package org.apache.cloudstack.api.command;
|
|||
|
||||
import com.cloud.utils.Pair;
|
||||
|
||||
import org.apache.cloudstack.api.ACL;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
|
|
@ -26,8 +25,10 @@ import org.apache.cloudstack.api.Parameter;
|
|||
import org.apache.cloudstack.api.response.AccountResponse;
|
||||
import org.apache.cloudstack.api.response.DomainResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.api.response.ProjectResponse;
|
||||
import org.apache.cloudstack.api.response.QuotaCreditsResponse;
|
||||
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
|
|
@ -44,12 +45,15 @@ public class QuotaCreditsListCmd extends BaseCmd {
|
|||
@Inject
|
||||
QuotaResponseBuilder quotaResponseBuilder;
|
||||
|
||||
@ACL
|
||||
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.")
|
||||
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
|
||||
description = "ID of the Account for which the credit statement will be generated. Can not be specified with '" + ApiConstants.PROJECT_ID + "'.")
|
||||
private Long accountId;
|
||||
|
||||
@ACL
|
||||
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " +
|
||||
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class,
|
||||
description = "ID of the Project for which the credit statement will be generated. Can not be specified with '" + ApiConstants.ACCOUNT_ID + "'.")
|
||||
private Long projectId;
|
||||
|
||||
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which credit statement will be generated. " +
|
||||
"Available only for administrators.")
|
||||
private Long domainId;
|
||||
|
||||
|
|
@ -97,14 +101,18 @@ public class QuotaCreditsListCmd extends BaseCmd {
|
|||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public Boolean getRecursive() {
|
||||
return recursive;
|
||||
public boolean isRecursive() {
|
||||
return BooleanUtils.isTrue(recursive);
|
||||
}
|
||||
|
||||
public void setRecursive(Boolean recursive) {
|
||||
this.recursive = recursive;
|
||||
}
|
||||
|
||||
public Long getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
Pair<List<QuotaCreditsResponse>, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this);
|
||||
|
|
@ -116,7 +124,10 @@ public class QuotaCreditsListCmd extends BaseCmd {
|
|||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return -1;
|
||||
if (ObjectUtils.allNull(accountId, projectId)) {
|
||||
return -1;
|
||||
}
|
||||
return _accountService.finalizeAccountId(accountId, null, null, projectId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response;
|
|||
import com.cloud.user.User;
|
||||
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
|
||||
|
|
@ -54,7 +55,7 @@ public interface QuotaResponseBuilder {
|
|||
|
||||
Pair<List<QuotaSummaryResponse>, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd);
|
||||
|
||||
QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce);
|
||||
QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd);
|
||||
|
||||
List<QuotaEmailTemplateResponse> listQuotaEmailTemplates(QuotaEmailTemplateListCmd cmd);
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import com.cloud.domain.dao.DomainDao;
|
|||
import com.cloud.event.ActionEvent;
|
||||
import com.cloud.event.EventTypes;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.network.dao.IPAddressDao;
|
||||
import com.cloud.network.dao.IPAddressVO;
|
||||
import com.cloud.network.dao.NetworkDao;
|
||||
|
|
@ -67,6 +66,9 @@ import com.cloud.user.UserVO;
|
|||
import com.cloud.utils.DateUtil;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.utils.db.Transaction;
|
||||
import com.cloud.utils.db.TransactionCallback;
|
||||
import com.cloud.utils.db.TransactionLegacy;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.vm.VMInstanceVO;
|
||||
import com.cloud.vm.dao.VMInstanceDao;
|
||||
|
|
@ -77,6 +79,7 @@ import org.apache.cloudstack.api.InternalIdentity;
|
|||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
|
||||
|
|
@ -412,45 +415,52 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
QuotaTypes quotaType = QuotaTypes.listQuotaTypes().get(type);
|
||||
|
||||
QuotaStatementItemResponse item = new QuotaStatementItemResponse(type);
|
||||
item.setQuotaUsed(usageRecords.stream().map(QuotaUsageJoinVO::getQuotaUsed).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
|
||||
BigDecimal quotaUsed = usageRecords.stream().map(QuotaUsageJoinVO::getQuotaUsed).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
item.setQuotaUsed(quotaUsed);
|
||||
item.setUsageUnit(quotaType.getQuotaUnit());
|
||||
item.setUsageName(quotaType.getQuotaName());
|
||||
|
||||
setStatementItemResources(item, usageType, usageRecords, showResources);
|
||||
if (showResources) {
|
||||
setStatementItemResources(item, usageType, usageRecords);
|
||||
} else {
|
||||
List<QuotaStatementItemHistoryResponse> history = createQuotaConsumptionHistory(usageRecords, quotaUsed);
|
||||
item.setHistory(history);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List<QuotaUsageJoinVO> quotaUsageRecords, boolean showResources) {
|
||||
if (!showResources) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List<QuotaUsageJoinVO> quotaUsageRecords) {
|
||||
List<QuotaStatementItemResourceResponse> itemDetails = new ArrayList<>();
|
||||
|
||||
Map<Long, BigDecimal> quotaUsagesValuesAggregatedById = quotaUsageRecords
|
||||
Map<Long, List<QuotaUsageJoinVO>> quotaUsagesAggregatedByResourceId = quotaUsageRecords
|
||||
.stream()
|
||||
.filter(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType),
|
||||
Collectors.reducing(new BigDecimal(0), QuotaUsageJoinVO::getQuotaUsed, BigDecimal::add)
|
||||
.collect(Collectors.groupingBy(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType)
|
||||
));
|
||||
|
||||
for (Map.Entry<Long, BigDecimal> entry : quotaUsagesValuesAggregatedById.entrySet()) {
|
||||
QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse();
|
||||
|
||||
detail.setQuotaUsed(entry.getValue());
|
||||
|
||||
for (Map.Entry<Long, List<QuotaUsageJoinVO>> entry : quotaUsagesAggregatedByResourceId.entrySet()) {
|
||||
QuotaUsageResourceVO resource = getResourceFromIdAndType(entry.getKey(), usageType);
|
||||
|
||||
QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse();
|
||||
if (resource != null) {
|
||||
detail.setResourceId(resource.getUuid());
|
||||
detail.setDisplayName(resource.getName());
|
||||
detail.setRemoved(resource.isRemoved());
|
||||
} else {
|
||||
detail.setDisplayName("<untraceable>");
|
||||
|
||||
}
|
||||
BigDecimal quotaUsed = entry.getValue().stream()
|
||||
.map(QuotaUsageJoinVO::getQuotaUsed)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
List<QuotaStatementItemHistoryResponse> history = createQuotaConsumptionHistory(entry.getValue(), quotaUsed);
|
||||
detail.setQuotaUsed(quotaUsed);
|
||||
detail.setHistory(history);
|
||||
|
||||
itemDetails.add(detail);
|
||||
}
|
||||
|
||||
statementItem.setResources(itemDetails);
|
||||
}
|
||||
|
||||
|
|
@ -524,6 +534,34 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
return null;
|
||||
}
|
||||
|
||||
protected List<QuotaStatementItemHistoryResponse> createQuotaConsumptionHistory(List<QuotaUsageJoinVO> quotaUsage, BigDecimal quotaUsed) {
|
||||
if (quotaUsed.equals(BigDecimal.ZERO)) {
|
||||
logger.debug("Not generating Quota consumption history because the item has not consumed any Quota in the period.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Date, QuotaStatementItemHistoryResponse> history = new HashMap<>();
|
||||
for (QuotaUsageJoinVO record : quotaUsage) {
|
||||
if (ObjectUtils.anyNull(record.getUsageItemId(), record.getQuotaUsed())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QuotaStatementItemHistoryResponse item = history.computeIfAbsent(
|
||||
record.getEndDate(),
|
||||
key -> new QuotaStatementItemHistoryResponse()
|
||||
);
|
||||
if (item.getStartDate() == null || item.getStartDate().after(record.getStartDate())) {
|
||||
item.setStartDate(record.getStartDate());
|
||||
}
|
||||
item.setEndDate(record.getEndDate());
|
||||
item.setQuotaConsumed(item.getQuotaConsumed().add(record.getQuotaUsed()));
|
||||
|
||||
history.put(record.getEndDate(), item);
|
||||
}
|
||||
|
||||
return history.values().stream().sorted(Comparator.comparing(QuotaStatementItemHistoryResponse::getEndDate)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<List<QuotaTariffVO>, Integer> listQuotaTariffPlans(final QuotaTariffListCmd cmd) {
|
||||
Date startDate = cmd.getEffectiveDate();
|
||||
|
|
@ -661,51 +699,79 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) {
|
||||
public QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd) {
|
||||
Double value = cmd.getValue();
|
||||
if (value == null) {
|
||||
throw new InvalidParameterValueException("Please specify a valid amount of credits.");
|
||||
}
|
||||
|
||||
Long accountId = _accountMgr.finalizeAccountId(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId());
|
||||
AccountVO account = _accountDao.findById(accountId);
|
||||
Long domainId = account.getDomainId();
|
||||
|
||||
Date depositedOn = new Date();
|
||||
QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn);
|
||||
|
||||
if (qb != null) {
|
||||
throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.",
|
||||
depositedOn));
|
||||
}
|
||||
|
||||
QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy);
|
||||
credits.setUpdatedOn(depositedOn);
|
||||
QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits);
|
||||
if (result == null) {
|
||||
logger.error("Unable to add credits to account ID [{}].", accountId);
|
||||
throw new CloudRuntimeException("Unable to add credits to account.");
|
||||
}
|
||||
|
||||
final AccountVO account = _accountDao.findById(accountId);
|
||||
if (account == null) {
|
||||
throw new InvalidParameterValueException("Account does not exist with account id " + accountId);
|
||||
}
|
||||
final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value());
|
||||
final BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId);
|
||||
logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount,
|
||||
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance);
|
||||
// update quota account with the balance
|
||||
_quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn);
|
||||
if (lockAccountEnforcement) {
|
||||
if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) {
|
||||
if (account.getState() == Account.State.LOCKED) {
|
||||
logger.info("UnLocking account " + account.getAccountName() + " , due to positive balance " + currentAccountBalance);
|
||||
_accountMgr.enableAccount(account.getAccountName(), domainId, accountId);
|
||||
}
|
||||
} else { // currentAccountBalance < 0 then lock the account
|
||||
if (_quotaManager.isLockable(account) && account.getState() == Account.State.ENABLED && enforce) {
|
||||
logger.info("Locking account " + account.getAccountName() + " , due to negative balance " + currentAccountBalance);
|
||||
_accountMgr.lockAccount(account.getAccountName(), domainId, accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value());
|
||||
QuotaCreditsVO result = Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<QuotaCreditsVO>) status -> persistQuotaCredits(cmd, value, depositedOn, account, lockAccountEnforcement));
|
||||
|
||||
UserVO creditor = getCreditorForQuotaCredits(result);
|
||||
return createQuotaCreditsResponse(result, creditor);
|
||||
}
|
||||
|
||||
protected QuotaCreditsVO persistQuotaCredits(QuotaCreditsCmd cmd, Double value, Date depositedOn, AccountVO account, boolean lockAccountEnforcement) {
|
||||
Long accountId = account.getId();
|
||||
Long domainId = account.getDomainId();
|
||||
long callingUserId = CallContext.current().getCallingUserId();
|
||||
QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(value), callingUserId);
|
||||
credits.setUpdatedOn(depositedOn);
|
||||
QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits);
|
||||
|
||||
BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId);
|
||||
logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", value,
|
||||
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance);
|
||||
_quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn);
|
||||
|
||||
Boolean enforceQuota = cmd.getQuotaEnforce();
|
||||
if (enforceQuota != null) {
|
||||
_quotaService.setLockAccount(accountId, enforceQuota);
|
||||
}
|
||||
|
||||
Double minBalance = cmd.getMinBalance();
|
||||
if (minBalance != null) {
|
||||
_quotaService.setMinBalance(accountId, minBalance);
|
||||
}
|
||||
|
||||
if (lockAccountEnforcement) {
|
||||
lockOrUnlockAccountIfRequired(currentAccountBalance, account, enforceQuota);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void lockOrUnlockAccountIfRequired(BigDecimal currentAccountBalance, AccountVO account, Boolean enforceQuota) {
|
||||
Long accountId = account.getId();
|
||||
Long domainId = account.getDomainId();
|
||||
String accountName = account.getAccountName();
|
||||
|
||||
if (currentAccountBalance.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
if (account.getState() == Account.State.LOCKED) {
|
||||
logger.info("Unlocking Account [{}] due to positive balance.", accountName);
|
||||
_accountMgr.enableAccount(accountName, domainId, accountId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(enforceQuota) && account.getState() == Account.State.ENABLED && _quotaManager.isLockable(account)) {
|
||||
logger.info("Locking Account [{}] due to negative balance.", accountName);
|
||||
_accountMgr.lockAccount(accountName, domainId, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) {
|
||||
QuotaEmailTemplateResponse response = new QuotaEmailTemplateResponse();
|
||||
response.setTemplateType(template.getTemplateName());
|
||||
|
|
@ -1036,26 +1102,16 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
}
|
||||
|
||||
protected List<QuotaCreditsVO> getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) {
|
||||
Long accountId = cmd.getAccountId();
|
||||
Long domainId = cmd.getDomainId();
|
||||
Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null);
|
||||
Pair<Long, List<Long>> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive());
|
||||
Date startDate = cmd.getStartDate();
|
||||
Date endDate = cmd.getEndDate();
|
||||
boolean isRecursive = cmd.getRecursive();
|
||||
|
||||
if (ObjectUtils.allNull(accountId, domainId)) {
|
||||
throw new InvalidParameterValueException("Please provide either account ID or domain ID.");
|
||||
}
|
||||
|
||||
if (startDate.after(endDate)) {
|
||||
throw new InvalidParameterValueException("The start date must be before the end date.");
|
||||
}
|
||||
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) {
|
||||
throw new PermissionDeniedException("Regular users are not allowed to generate domain statements.");
|
||||
}
|
||||
|
||||
return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive);
|
||||
return quotaCreditsDao.findCredits(accountId, baseDomainAndFilteredDomains.second(), startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1208,6 +1264,14 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the appropriate Account ID to use for Quota statement-related operations while ensuring correct permissions.
|
||||
*
|
||||
* @param providedAccountId the ID of the Account provided to the command.
|
||||
* @param fallbackAccountId the ID of a fallback Account to use for User Accounts if no specific Account ID was provided.
|
||||
* If null, then we fallback to the User Account itself.
|
||||
* @return the account ID to be used for the Quota statement, or null if no specific Account limitation is required.
|
||||
*/
|
||||
protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) {
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
|
||||
|
|
@ -1235,6 +1299,17 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
|
|||
return caller.getAccountId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the Domains for which a Quota statement should be generated while ensuring correct permissions.
|
||||
*
|
||||
* @param finalAccountId the Account ID determined via <code>org.apache.cloudstack.api.response.QuotaResponseBuilderImpl#getAccountIdForQuotaStatement(long, java.lang.Long)</code>.
|
||||
* @param providedDomainId the Domain ID provided to the command.
|
||||
* @param isRecursive the recursion flag provided to the command.
|
||||
* @return A pair containing:
|
||||
* - The base Domain's ID as the first element. This can be null if we are not limiting by Domain.
|
||||
* - A list containing the base Domain's ID and optionally its children if
|
||||
* the recursion flag is true. Also nullable.
|
||||
*/
|
||||
protected Pair<Long, List<Long>> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) {
|
||||
if (finalAccountId != null) {
|
||||
// Access to the provided account has already been validated
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
//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.
|
||||
package org.apache.cloudstack.api.response;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
public class QuotaStatementItemHistoryResponse extends BaseResponse {
|
||||
|
||||
@SerializedName(ApiConstants.START_DATE)
|
||||
@Param(description = "Start date of the item.")
|
||||
private Date startDate = null;
|
||||
|
||||
@SerializedName(ApiConstants.END_DATE)
|
||||
@Param(description = "End date of the item.")
|
||||
private Date endDate = null;
|
||||
|
||||
@SerializedName(ApiConstants.QUOTA_CONSUMED)
|
||||
@Param(description = "Amount of quota consumed.")
|
||||
private BigDecimal quotaConsumed = BigDecimal.ZERO;
|
||||
|
||||
public QuotaStatementItemHistoryResponse() {
|
||||
}
|
||||
|
||||
public Date getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(Date startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public Date getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(Date endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public BigDecimal getQuotaConsumed() {
|
||||
return quotaConsumed;
|
||||
}
|
||||
|
||||
public void setQuotaConsumed(BigDecimal quotaConsumed) {
|
||||
this.quotaConsumed = quotaConsumed;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package org.apache.cloudstack.api.response;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
|
|
@ -43,6 +44,10 @@ public class QuotaStatementItemResourceResponse extends BaseResponse {
|
|||
@Param(description = "Indicates whether the resource is removed or active.")
|
||||
private boolean removed;
|
||||
|
||||
@SerializedName(ApiConstants.HISTORY)
|
||||
@Param(description = "Quota consumption history.")
|
||||
private List<QuotaStatementItemHistoryResponse> history;
|
||||
|
||||
public void setQuotaUsed(BigDecimal quotaUsed) {
|
||||
this.quotaUsed = quotaUsed;
|
||||
}
|
||||
|
|
@ -58,4 +63,9 @@ public class QuotaStatementItemResourceResponse extends BaseResponse {
|
|||
public void setRemoved(boolean removed) {
|
||||
this.removed = removed;
|
||||
}
|
||||
|
||||
public void setHistory(List<QuotaStatementItemHistoryResponse> history) {
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ public class QuotaStatementItemResponse extends BaseResponse {
|
|||
@Param(description = "Item's resources.")
|
||||
private List<QuotaStatementItemResourceResponse> resources;
|
||||
|
||||
@SerializedName(ApiConstants.HISTORY)
|
||||
@Param(description = "Quota consumption history.")
|
||||
private List<QuotaStatementItemHistoryResponse> history;
|
||||
|
||||
public QuotaStatementItemResponse(final int usageType) {
|
||||
this.usageType = usageType;
|
||||
}
|
||||
|
|
@ -92,4 +96,7 @@ public class QuotaStatementItemResponse extends BaseResponse {
|
|||
this.resources = resources;
|
||||
}
|
||||
|
||||
public void setHistory(List<QuotaStatementItemHistoryResponse> history) {
|
||||
this.history = history;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,9 @@
|
|||
// under the License.
|
||||
package org.apache.cloudstack.api.command;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.QuotaCreditsResponse;
|
||||
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
|
||||
import org.apache.cloudstack.quota.QuotaService;
|
||||
|
|
@ -78,23 +71,13 @@ public class QuotaCreditsCmdTest extends TestCase {
|
|||
AccountVO acc = new AccountVO();
|
||||
acc.setId(2L);
|
||||
|
||||
Mockito.when(accountService.getActiveAccountByName(nullable(String.class), nullable(Long.class))).thenReturn(acc);
|
||||
|
||||
Mockito.when(responseBuilder.addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class))).thenReturn(new QuotaCreditsResponse());
|
||||
|
||||
// No value provided test
|
||||
try {
|
||||
cmd.execute();
|
||||
} catch (ServerApiException e) {
|
||||
assertTrue(e.getErrorCode().equals(ApiErrorCode.PARAM_ERROR));
|
||||
}
|
||||
Mockito.when(responseBuilder.addQuotaCredits(cmd)).thenReturn(new QuotaCreditsResponse());
|
||||
|
||||
// With value provided test
|
||||
cmd.setValue(11.80);
|
||||
cmd.execute();
|
||||
Mockito.verify(quotaService, Mockito.times(0)).setLockAccount(anyLong(), anyBoolean());
|
||||
Mockito.verify(quotaService, Mockito.times(1)).setMinBalance(anyLong(), anyDouble());
|
||||
Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class));
|
||||
|
||||
Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(cmd);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import org.apache.cloudstack.api.InternalIdentity;
|
|||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
|
||||
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
|
||||
|
|
@ -52,6 +53,7 @@ import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
|
|||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.discovery.ApiDiscoveryService;
|
||||
import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
|
||||
import org.apache.cloudstack.quota.QuotaManager;
|
||||
import org.apache.cloudstack.quota.QuotaService;
|
||||
import org.apache.cloudstack.quota.QuotaStatement;
|
||||
import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition;
|
||||
|
|
@ -192,6 +194,12 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
@Mock
|
||||
EntityManager entityManagerMock;
|
||||
|
||||
@Mock
|
||||
QuotaManager quotaManagerMock;
|
||||
|
||||
@Mock
|
||||
QuotaBalanceVO quotaBalanceVoMock;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
CallContext.register(callerUserMock, callerAccountMock);
|
||||
|
|
@ -243,28 +251,6 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
assertNull(tariffResponse.getActivationRule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddQuotaCredits() {
|
||||
final long accountId = 2L;
|
||||
final long domainId = 1L;
|
||||
final double amount = 11.0;
|
||||
final long updatedBy = 2L;
|
||||
|
||||
QuotaCreditsVO credit = new QuotaCreditsVO();
|
||||
credit.setCredit(new BigDecimal(amount));
|
||||
|
||||
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit);
|
||||
Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(Mockito.anyLong(), Mockito.anyLong())).thenReturn(new BigDecimal(111));
|
||||
Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit);
|
||||
|
||||
AccountVO account = new AccountVO();
|
||||
account.setState(Account.State.LOCKED);
|
||||
Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account);
|
||||
|
||||
QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true);
|
||||
assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListQuotaEmailTemplates() {
|
||||
QuotaEmailTemplateListCmd cmd = new QuotaEmailTemplateListCmd();
|
||||
|
|
@ -710,33 +696,18 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
}
|
||||
|
||||
private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() {
|
||||
Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong());
|
||||
QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
|
||||
cmd.setAccountId(1L);
|
||||
cmd.setDomainId(2L);
|
||||
QuotaCreditsListCmd cmd = Mockito.mock(QuotaCreditsListCmd.class);
|
||||
Mockito.doReturn(1L).when(cmd).getEntityOwnerId();
|
||||
Mockito.doReturn(2L).when(cmd).getDomainId();
|
||||
Mockito.doReturn(new Date()).when(cmd).getStartDate();
|
||||
Mockito.doReturn(new Date()).when(cmd).getEndDate();
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() {
|
||||
QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
|
||||
|
||||
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() {
|
||||
QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
|
||||
cmd.setStartDate(new Date());
|
||||
cmd.setEndDate(DateUtils.addDays(new Date(), -1));
|
||||
|
||||
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
|
||||
}
|
||||
|
||||
@Test(expected = PermissionDeniedException.class)
|
||||
public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() {
|
||||
QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
|
||||
Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong());
|
||||
Mockito.doReturn(DateUtils.addDays(new Date(), -1)).when(cmd).getEndDate();
|
||||
|
||||
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
|
||||
}
|
||||
|
|
@ -747,7 +718,7 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
List<QuotaCreditsVO> expected = new ArrayList<>();
|
||||
expected.add(new QuotaCreditsVO());
|
||||
|
||||
Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean());
|
||||
Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
|
||||
|
||||
List<QuotaCreditsVO> result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
|
||||
|
||||
|
|
@ -1007,7 +978,6 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
@Test
|
||||
public void createStatementItemTestReturnItem() {
|
||||
List<QuotaUsageJoinVO> quotaUsages = getQuotaUsagesForTest();
|
||||
Mockito.doNothing().when(quotaResponseBuilderSpy).setStatementItemResources(Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.anyBoolean());
|
||||
|
||||
QuotaStatementItemResponse result = quotaResponseBuilderSpy.createStatementItem(0, quotaUsages, false);
|
||||
|
||||
|
|
@ -1018,15 +988,6 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
Assert.assertEquals(quotaTypeExpected.getQuotaName(), result.getUsageName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setStatementItemResourcesTestDoNotShowResourcesDoNothing() {
|
||||
QuotaStatementItemResponse item = new QuotaStatementItemResponse(1);
|
||||
|
||||
quotaResponseBuilderSpy.setStatementItemResources(item, 0, getQuotaUsagesForTest(), false);
|
||||
|
||||
Assert.assertNull(item.getResources());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() {
|
||||
long providedAccountId = 200L;
|
||||
|
|
@ -1185,4 +1146,226 @@ public class QuotaResponseBuilderImplTest extends TestCase {
|
|||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(mockResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockOrUnlockAccountIfRequiredTestPositiveBalanceUnlocksAccount() {
|
||||
Mockito.doReturn(Account.State.LOCKED).when(accountMock).getState();
|
||||
|
||||
quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.TEN, accountMock, true);
|
||||
|
||||
Mockito.verify(accountManagerMock).enableAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId());
|
||||
Mockito.verify(accountManagerMock, Mockito.never()).lockAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockOrUnlockAccountIfRequiredTestNegativeBalanceLocksAccount() {
|
||||
Mockito.doReturn(Account.State.ENABLED).when(accountMock).getState();
|
||||
Mockito.doReturn(true).when(quotaManagerMock).isLockable(accountMock);
|
||||
|
||||
quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.valueOf(-10), accountMock, true);
|
||||
|
||||
Mockito.verify(accountManagerMock).lockAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId());
|
||||
Mockito.verify(accountManagerMock, Mockito.never()).enableAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addQuotaCreditsTestValidParameters() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Mockito.doReturn(10D).when(cmd).getValue();
|
||||
Mockito.doReturn(BigDecimal.TEN).when(quotaCreditsVoMock).getCredit();
|
||||
Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong());
|
||||
Mockito.doReturn(null).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(),
|
||||
Mockito.any());
|
||||
Mockito.doReturn(quotaCreditsVoMock).when(quotaResponseBuilderSpy).persistQuotaCredits(Mockito.any(), Mockito.anyDouble(),
|
||||
Mockito.any(), Mockito.any(), Mockito.anyBoolean());
|
||||
Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(Mockito.any());
|
||||
|
||||
QuotaCreditsResponse response = quotaResponseBuilderSpy.addQuotaCredits(cmd);
|
||||
|
||||
Assert.assertEquals(BigDecimal.TEN, response.getCredit());
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void addQuotaCreditsTestThrowsExceptionWhenValueIsNull() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Mockito.doReturn(null).when(cmd).getValue();
|
||||
|
||||
quotaResponseBuilderSpy.addQuotaCredits(cmd);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void addQuotaCreditsTestThrowsExceptionWhenDepositDateIsIncorrect() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Mockito.doReturn(100.0).when(cmd).getValue();
|
||||
Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong());
|
||||
Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
|
||||
|
||||
quotaResponseBuilderSpy.addQuotaCredits(cmd);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistQuotaCreditsTestSavesCreditsAndBalanceSuccessfully() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Long accountId = 1L;
|
||||
Long domainId = 2L;
|
||||
Double value = 10D;
|
||||
Date depositedOn = new Date();
|
||||
AccountVO account = Mockito.mock(AccountVO.class);
|
||||
BigDecimal currentBalance = BigDecimal.ZERO;
|
||||
|
||||
Mockito.doReturn(accountId).when(account).getId();
|
||||
Mockito.doReturn(domainId).when(account).getDomainId();
|
||||
Mockito.doReturn(null).when(cmd).getQuotaEnforce();
|
||||
Mockito.doReturn(null).when(cmd).getMinBalance();
|
||||
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance);
|
||||
|
||||
QuotaCreditsVO result = quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(BigDecimal.TEN, result.getCredit());
|
||||
Assert.assertEquals(accountId, result.getAccountId());
|
||||
Assert.assertEquals(depositedOn, result.getUpdatedOn());
|
||||
Mockito.verify(quotaServiceMock).saveQuotaAccount(account, currentBalance, depositedOn);
|
||||
Mockito.verify(quotaServiceMock, Mockito.never()).setLockAccount(Mockito.anyLong(), Mockito.anyBoolean());
|
||||
Mockito.verify(quotaServiceMock, Mockito.never()).setMinBalance(Mockito.anyLong(), Mockito.anyDouble());
|
||||
Mockito.verify(quotaResponseBuilderSpy, Mockito.never()).lockOrUnlockAccountIfRequired(Mockito.any(), Mockito.any(), Mockito.anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistQuotaCreditsTestCallsSetLockAccountWhenQuotaEnforceProvided() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Long accountId = 1L;
|
||||
Double value = 100.0;
|
||||
Date depositedOn = new Date();
|
||||
AccountVO account = Mockito.mock(AccountVO.class);
|
||||
|
||||
Mockito.doReturn(accountId).when(account).getId();
|
||||
Mockito.when(cmd.getQuotaEnforce()).thenReturn(Boolean.TRUE);
|
||||
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false);
|
||||
|
||||
Mockito.verify(quotaServiceMock).setLockAccount(accountId, Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistQuotaCreditsTestCallsSetMinBalanceWhenProvided() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Long accountId = 1L;
|
||||
Double value = 100.0;
|
||||
Date depositedOn = new Date();
|
||||
AccountVO account = Mockito.mock(AccountVO.class);
|
||||
|
||||
Mockito.when(cmd.getMinBalance()).thenReturn(50.0);
|
||||
Mockito.doReturn(accountId).when(account).getId();
|
||||
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false);
|
||||
|
||||
Mockito.verify(quotaServiceMock).setMinBalance(accountId, 50.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistQuotaCreditsTestLocksOrUnlocksAccountWhenEnforcementIsEnabledGlobally() {
|
||||
QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class);
|
||||
Long accountId = 1L;
|
||||
Long domainId = 2L;
|
||||
Double value = 100.0;
|
||||
Date depositedOn = new Date();
|
||||
AccountVO account = Mockito.mock(AccountVO.class);
|
||||
BigDecimal currentBalance = BigDecimal.ZERO;
|
||||
|
||||
Mockito.doReturn(accountId).when(account).getId();
|
||||
Mockito.doReturn(domainId).when(account).getDomainId();
|
||||
Mockito.when(cmd.getMinBalance()).thenReturn(50.0);
|
||||
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance);
|
||||
|
||||
quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, true);
|
||||
|
||||
Mockito.verify(quotaResponseBuilderSpy).lockOrUnlockAccountIfRequired(currentBalance, account, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createQuotaConsumptionHistoryTestReturnsNullForZeroQuotaUsed() {
|
||||
List<QuotaStatementItemHistoryResponse> result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(new ArrayList<>(), BigDecimal.ZERO);
|
||||
|
||||
Assert.assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createQuotaConsumptionHistoryTestIgnoresNullQuotaUsed() {
|
||||
Date now = new Date();
|
||||
|
||||
List<QuotaUsageJoinVO> usageRecords = new ArrayList<>();
|
||||
QuotaUsageJoinVO record1 = new QuotaUsageJoinVO();
|
||||
record1.setStartDate(now);
|
||||
record1.setEndDate(now);
|
||||
record1.setQuotaUsed(null);
|
||||
record1.setUsageItemId(10L);
|
||||
|
||||
QuotaUsageJoinVO record2 = new QuotaUsageJoinVO();
|
||||
record2.setStartDate(new Date(now.getTime() + 1000));
|
||||
record2.setEndDate(new Date(now.getTime() + 1000));
|
||||
record2.setQuotaUsed(BigDecimal.valueOf(10));
|
||||
record2.setUsageItemId(11L);
|
||||
|
||||
usageRecords.add(record1);
|
||||
usageRecords.add(record2);
|
||||
|
||||
BigDecimal totalQuotaUsed = BigDecimal.valueOf(10);
|
||||
|
||||
List<QuotaStatementItemHistoryResponse> result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(1, result.size());
|
||||
Assert.assertEquals(BigDecimal.valueOf(10), result.get(0).getQuotaConsumed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createQuotaConsumptionHistoryTestCorrectlyAggregatesRecords() {
|
||||
List<QuotaUsageJoinVO> usageRecords = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
|
||||
QuotaUsageJoinVO record1 = new QuotaUsageJoinVO();
|
||||
record1.setStartDate(now);
|
||||
record1.setEndDate(new Date(now.getTime() + 1000));
|
||||
record1.setQuotaUsed(BigDecimal.valueOf(5));
|
||||
record1.setUsageItemId(10L);
|
||||
|
||||
QuotaUsageJoinVO record2 = new QuotaUsageJoinVO();
|
||||
record2.setStartDate(new Date(now.getTime() + 2000));
|
||||
record2.setEndDate(new Date(now.getTime() + 3000));
|
||||
record2.setQuotaUsed(BigDecimal.valueOf(15));
|
||||
record2.setUsageItemId(11L);
|
||||
|
||||
QuotaUsageJoinVO record3 = new QuotaUsageJoinVO();
|
||||
record3.setStartDate(new Date(now.getTime() + 2000));
|
||||
record3.setEndDate(new Date(now.getTime() + 3000));
|
||||
record3.setQuotaUsed(BigDecimal.valueOf(5));
|
||||
record3.setUsageItemId(11L);
|
||||
|
||||
usageRecords.add(record1);
|
||||
usageRecords.add(record2);
|
||||
usageRecords.add(record3);
|
||||
|
||||
BigDecimal totalQuotaUsed = BigDecimal.valueOf(25);
|
||||
|
||||
List<QuotaStatementItemHistoryResponse> result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(2, result.size());
|
||||
|
||||
QuotaStatementItemHistoryResponse firstHistory = result.get(0);
|
||||
QuotaStatementItemHistoryResponse secondHistory = result.get(1);
|
||||
|
||||
Assert.assertEquals(BigDecimal.valueOf(5), firstHistory.getQuotaConsumed());
|
||||
Assert.assertEquals(record1.getStartDate(), firstHistory.getStartDate());
|
||||
Assert.assertEquals(record1.getEndDate(), firstHistory.getEndDate());
|
||||
|
||||
Assert.assertEquals(BigDecimal.valueOf(20), secondHistory.getQuotaConsumed());
|
||||
Assert.assertEquals(record2.getStartDate(), secondHistory.getStartDate());
|
||||
Assert.assertEquals(record2.getEndDate(), secondHistory.getEndDate());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3961,7 +3961,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
|||
if (getActiveAccountById(accountId) != null) {
|
||||
return accountId;
|
||||
}
|
||||
throw new InvalidParameterValueException(String.format("Unable to find account with the specified ID."));
|
||||
throw new InvalidParameterValueException("Unable to find account with the specified ID.");
|
||||
}
|
||||
|
||||
if (accountName == null && domainId == null) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ module.exports = {
|
|||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'<rootDir>/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design|@vue-js-cron)'
|
||||
'<rootDir>/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design|@vue-js-cron|prism)'
|
||||
],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -65,6 +65,7 @@
|
|||
"vue": "^3.2.31",
|
||||
"vue-chartjs": "^4.0.7",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
"vue-code-highlight": "^0.7.8",
|
||||
"vue-cropper": "^1.0.2",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-loader": "^16.8.3",
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@
|
|||
"label.action.vmstoragesnapshot.create": "Take Instance volume Snapshot",
|
||||
"label.actions": "Actions",
|
||||
"label.active": "Active",
|
||||
"label.activeaccounts": "Active accounts",
|
||||
"label.activate.project": "Activate project",
|
||||
"label.activeviewersessions": "Active sessions",
|
||||
"label.add": "Add",
|
||||
|
|
@ -669,6 +670,7 @@
|
|||
"label.consoleproxy": "Console proxy",
|
||||
"label.console.proxy": "Console proxy",
|
||||
"label.console.proxy.vm": "Console proxy VM",
|
||||
"label.consumption": "Consumption",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.continue.install": "Continue with installation",
|
||||
|
|
@ -737,6 +739,8 @@
|
|||
"label.creating": "Creating",
|
||||
"label.creating.iprange": "Creating IP ranges",
|
||||
"label.credit": "Credit",
|
||||
"label.credits": "Credits",
|
||||
"label.creditor": "Creditor",
|
||||
"label.cron": "Cron expression",
|
||||
"label.cron.mode": "Cron mode",
|
||||
"label.crosszones": "Cross Zones",
|
||||
|
|
@ -1025,6 +1029,7 @@
|
|||
"label.egressdefaultpolicy": "Default egress policy",
|
||||
"label.elastic": "Elastic",
|
||||
"label.email": "Email",
|
||||
"label.emailtemplate": "Email Template",
|
||||
"label.enable.autoscale.vmgroup": "Enable AutoScaling Group",
|
||||
"label.enable.csi": "Enable CloudStack CSI Driver",
|
||||
"label.enable.custom.action": "Enable Custom Action",
|
||||
|
|
@ -1096,6 +1101,9 @@
|
|||
"label.expungevmgraceperiod": "Expunge Instance grace period (in sec)",
|
||||
"label.expunged": "Expunged",
|
||||
"label.expunging": "Expunging",
|
||||
"label.export.data.csv": "Export data as CSV",
|
||||
"label.export.details.csv": "Export details as CSV",
|
||||
"label.export.resources.csv": "Export resources as CSV",
|
||||
"label.export.rules": "Export Rules",
|
||||
"label.ext.hostname.tooltip": "External Host Name or IP Address",
|
||||
"label.extension": "Extension",
|
||||
|
|
@ -2061,23 +2069,38 @@
|
|||
"label.quota": "Quota",
|
||||
"label.quota.add.credits": "Add credits",
|
||||
"label.quota.configuration": "Quota configuration",
|
||||
"label.quota.credits": "Credits",
|
||||
"label.quota.consumed": "Quota consumed",
|
||||
"label.quota.current.balance": "Current balance",
|
||||
"label.quota.email.edit": "Edit Email Template",
|
||||
"label.quota.enforce": "Enforce Quota",
|
||||
"label.quota.filter.period": "Filtering data from <b>{startDate}</b> to <b>{endDate}</b>",
|
||||
"label.quota.last.balance": "Last balance at the day",
|
||||
"label.quota.period.today": "Today",
|
||||
"label.quota.period.this.week": "This week",
|
||||
"label.quota.period.this.month": "This month",
|
||||
"label.quota.period.last.month": "Last month",
|
||||
"label.quota.period.this.year": "This year",
|
||||
"label.quota.period.last.year": "Last year",
|
||||
"label.quota.period.custom": "Custom",
|
||||
"label.quota.select.period": "Select a period",
|
||||
"label.quota.statement": "Statement",
|
||||
"label.quota.statement.balance": "Quota balance",
|
||||
"label.quota.statement.quota": "Quota usage",
|
||||
"label.quota.statement.history": "History (non-cumulative)",
|
||||
"label.quota.statement.cumulative.history": "History (cumulative)",
|
||||
"label.quota.statement.tariff": "Quota tariff",
|
||||
"label.quota.summary": "Summary",
|
||||
"label.quota.tariff": "Tariff",
|
||||
"label.quota.tariff.activationrule": "Activation rule",
|
||||
"label.quota.tariff.effectivedate": "Effective date",
|
||||
"label.quota.tariff.hasactivationrule": "Has activation rule?",
|
||||
"label.quota.tariff.position": "Position",
|
||||
"label.quota.tariff.value": "Tariff value",
|
||||
"label.quota.total": "Total",
|
||||
"label.quota.total.consumption": "Total quota consumption",
|
||||
"label.quota.type.name": "Usage Type",
|
||||
"label.quota.type.unit": "Usage unit",
|
||||
"label.quota.usage": "Quota consumption",
|
||||
"label.quota.usage.types.summary": "Usage types summary",
|
||||
"label.quota.usage.resources.by.type": "Resources by usage type",
|
||||
"label.quota.usage.details.by.resource": "Details by resource",
|
||||
"label.quota.validate.activation.rule": "Validate activation rule",
|
||||
"label.quota.value": "Quota value",
|
||||
"label.quotastate": "Quota state",
|
||||
|
|
@ -2173,6 +2196,7 @@
|
|||
"label.remove.vpc": "Remove VPC",
|
||||
"label.remove.vpc.offering": "Remove VPC Offering",
|
||||
"label.removed": "Removed",
|
||||
"label.removedaccounts": "Removed Accounts",
|
||||
"label.removing": "Removing",
|
||||
"label.replace": "Replace",
|
||||
"label.replace.acl": "Replace ACL",
|
||||
|
|
@ -3087,6 +3111,10 @@
|
|||
"message.action.primary.storage.scope.cluster": "Please confirm that you want to change the scope from Zone to the specified Cluster.<br>This operation will update the database and disconnect the storage pool from all hosts that were previously connected to the primary storage and are not part of the specified cluster.",
|
||||
"message.action.primary.storage.scope.zone": "Please confirm that you want to change the scope from Cluster to Zone.<br>This operation will update the database and connect the storage pool to all hosts of the zone running the same hypervisor as set on the storage pool.",
|
||||
"message.action.primarystorage.enable.maintenance.mode": "Warning: placing the primary storage into maintenance mode will cause all Instances using volumes from it to be stopped. Do you want to continue?",
|
||||
"message.action.quota.credit.add.error.accountrequired": "Please, inform the account",
|
||||
"message.action.quota.credit.add.error.domainidrequired": "Please, inform the domain",
|
||||
"message.action.quota.credit.add.error.valuerequired": "Please, inform the amount of credits",
|
||||
"message.action.quota.credit.add.success": "Successfully added {credit} credits to account \"{account}\"",
|
||||
"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.",
|
||||
|
|
@ -3835,6 +3863,7 @@
|
|||
"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.quota.usage.resource.warn": "Resources that are tagged as <untraceable> do not have constant metadata (if removed, the data is deleted) and cannot be retrieved.",
|
||||
"message.read.accept.license.agreements": "Please read and accept the terms for the license agreements.",
|
||||
"message.read.admin.guide.scaling.up": "Please read the dynamic scaling section in the admin guide before scaling up.",
|
||||
"message.recover.vm": "Please confirm that you would like to recover this Instance.",
|
||||
|
|
@ -3869,6 +3898,7 @@
|
|||
"message.remove.sticky.policy.processing": "Removing sticky policy...",
|
||||
"message.remove.vpc": "Please confirm that you want to remove the VPC",
|
||||
"message.request.failed": "Request failed.",
|
||||
"message.request.no.data": "There is no data to show.",
|
||||
"message.required.add.least.ip": "Please add at least 1 IP Range",
|
||||
"message.required.traffic.type": "All required traffic types should be added and with multiple physical networks each traffic type should have a label.",
|
||||
"message.required.tagged.physical.network": "There can only be one untagged physical network with guest traffic type.",
|
||||
|
|
@ -4241,6 +4271,8 @@
|
|||
"migrate.from": "Migrate from",
|
||||
"migrate.to": "Migrate to",
|
||||
"migrationPolicy": "Migration policy",
|
||||
"placeholder.quota.credit.add.min_balance": "Account's minimum balance",
|
||||
"placeholder.quota.credit.add.value": "Amount of credits",
|
||||
"placeholder.quota.tariff.activationrule": "Quota tariff's activation rule",
|
||||
"placeholder.quota.tariff.description": "Quota tariff's description",
|
||||
"placeholder.quota.tariff.enddate": "Quota tariff's end date",
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@
|
|||
"label.action.vmstoragesnapshot.create": "Criar snapshot de volume da VM",
|
||||
"label.actions": "A\u00e7\u00f5es",
|
||||
"label.active": "Ativo",
|
||||
"label.activeaccounts": "Contas ativas",
|
||||
"label.activate.project": "Ativar projeto",
|
||||
"label.activeviewersessions": "Sess\u00f5es ativas",
|
||||
"label.add": "Adicionar",
|
||||
|
|
@ -606,6 +607,7 @@
|
|||
"label.consoleproxy": "Console proxy",
|
||||
"label.console.proxy": "Console proxy",
|
||||
"label.console.proxy.vm": "VM da console proxy",
|
||||
"label.consumption": "Consumo",
|
||||
"label.continue": "Continuar",
|
||||
"label.continue.install": "Continuar com a instala\u00e7\u00e3o",
|
||||
"label.controlnodes": "Controlar nodos",
|
||||
|
|
@ -667,6 +669,8 @@
|
|||
"label.creating": "Criando",
|
||||
"label.creating.iprange": "Criando intervalos de IP",
|
||||
"label.credit": "Cr\u00e9dito",
|
||||
"label.credits": "Cr\u00e9ditos",
|
||||
"label.creditor": "Credor",
|
||||
"label.cron": "Express\u00e3o Cron",
|
||||
"label.cron.mode": "Modo Cron",
|
||||
"label.crosszones": "Inter zonas",
|
||||
|
|
@ -939,7 +943,8 @@
|
|||
"label.egress.rules": "Regras de sa\u00edda",
|
||||
"label.egressdefaultpolicy": "Pol\u00edtica padr\u00e3o de egress\u00e3o",
|
||||
"label.elastic": "El\u00e1stico",
|
||||
"label.email": "Email",
|
||||
"label.email": "E-mail",
|
||||
"label.emailtemplate": "Template de e-mail",
|
||||
"label.enable.host": "Habilita host",
|
||||
"label.enable.network.offering": "Habilita oferta de rede",
|
||||
"label.enable.oauth": "Ativar Login OAuth",
|
||||
|
|
@ -1490,7 +1495,7 @@
|
|||
"label.migrate.instance.single.storage": "Migrar todo(s) o(s) volume(s) da Inst\u00e2ncia para um \u00fanico armazenamento prim\u00e1rio",
|
||||
"label.migrate.instance.specific.storages": "Migrar volume(s) da Inst\u00e2ncia para armazenamentos prim\u00e1rios espec\u00edficos",
|
||||
"label.migrate.with.storage": "Migrar com armazenamento",
|
||||
"label.min_balance": "Saldo m\u00edn",
|
||||
"label.min_balance": "Saldo m\u00ednimo",
|
||||
"label.minimumsemanticversion": "Vers\u00e3o sem\u00e2ntica m\u00ednima",
|
||||
"label.minmembers": "M\u00edn membros",
|
||||
"label.minorsequence": "Sequ\u00eancia Menor",
|
||||
|
|
@ -1836,27 +1841,43 @@
|
|||
"label.quota": "Cota",
|
||||
"label.quota.add.credits": "Adicionar cr\u00e9ditos",
|
||||
"label.quota.configuration": "Configura\u00e7\u00e3o da cota",
|
||||
"label.quota.credits": "Cr\u00e9ditos",
|
||||
"label.quota.email.edit": "Editar template de email",
|
||||
"label.quota.consumed": "Cota consumida",
|
||||
"label.quota.current.balance": "Saldo atual",
|
||||
"label.quota.email.edit": "Editar template de e-mail",
|
||||
"label.quota.filter.period": "Filtrando dados entre <b>{startDate}</b> e <b>{endDate}</b>",
|
||||
"label.quota.last.balance": "Hor\u00e1rio do \u00faltimo balan\u00e7o do dia",
|
||||
"label.quota.period.today": "Hoje",
|
||||
"label.quota.period.this.week": "Essa semana",
|
||||
"label.quota.period.this.month": "Esse m\u00eas",
|
||||
"label.quota.period.last.month": "M\u00eas passado",
|
||||
"label.quota.period.this.year": "Esse ano",
|
||||
"label.quota.period.last.year": "Ano passado",
|
||||
"label.quota.period.custom": "Personalizado",
|
||||
"label.quota.enforce": "Impor cota",
|
||||
"label.quota.select.period": "Selecione um per\u00edodo",
|
||||
"label.quota.statement": "Demonstrativo",
|
||||
"label.quota.statement.balance": "Saldo",
|
||||
"label.quota.statement.quota": "Utiliza\u00e7\u00e3o",
|
||||
"label.quota.statement.history": "Hist\u00f3rico (n\u00e3o-cumulativo)",
|
||||
"label.quota.statement.cumulative.history": "Hist\u00f3rico (cumulativo)",
|
||||
"label.quota.statement.tariff": "Tarifa",
|
||||
"label.quota.summary": "Relat\u00f3rios",
|
||||
"label.quotastate": "Estado da cota",
|
||||
"label.quotastate": "Estado do Quota",
|
||||
"label.quota_enforce": "Impor Cota",
|
||||
"label.summary": "Sum\u00e1rio",
|
||||
"label.quota.tariff": "Tarifa",
|
||||
"label.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o",
|
||||
"label.quota.tariff.effectivedate": "Data efetiva",
|
||||
"label.quota.tariff.hasactivationrule": "Possui regra de ativa\u00e7\u00e3o?",
|
||||
"label.quota.tariff.position": "Posi\u00e7\u00e3o",
|
||||
"label.quota.tariff.value": "Valor",
|
||||
"label.quota.total": "Total",
|
||||
"label.quota.total.consumption": "Consumo total",
|
||||
"label.quota.type.name": "Tipo de uso",
|
||||
"label.quota.type.unit": "Unidade do uso",
|
||||
"label.action.update.object.storage" : "Atualizar Object Storage",
|
||||
"label.quota.usage": "Consumo da cota",
|
||||
"label.quota.usage.types.summary": "Sum\u00e1rio dos tipos",
|
||||
"label.quota.usage.resources.by.type": "Recursos por tipo",
|
||||
"label.quota.usage.details.by.resource": "Detalhes por recurso",
|
||||
"label.quota.validate.activation.rule": "Validar regra de ativa\u00e7\u00e3o",
|
||||
"label.quota.value": "Valor",
|
||||
"label.rados.monitor": "Monitor RADOS",
|
||||
|
|
@ -2298,14 +2319,12 @@
|
|||
"label.tagged": "Etiquetado",
|
||||
"label.tags": "Etiquetas",
|
||||
"label.target.iqn": "IQN alvo",
|
||||
"label.tariffactions": "A\u00e7\u00f5es",
|
||||
"label.tariffvalue": "Valor da tarifa",
|
||||
"label.tcp": "TCP",
|
||||
"label.tcp.proxy": "TCP proxy",
|
||||
"label.template": "Template",
|
||||
"label.template.select.existing": "Selecione um template existente",
|
||||
"label.template.temporary.import": "Utilize um template tempor\u00e1rio para importar",
|
||||
"label.templatebody": "Corpo do email",
|
||||
"label.templatebody": "Corpo do e-mail",
|
||||
"label.templatefileupload": "Arquivo local",
|
||||
"label.templateid": "Selecione um template",
|
||||
"label.templateiso": "Template/ISO",
|
||||
|
|
@ -2750,6 +2769,10 @@
|
|||
"message.action.primary.storage.scope.cluster": "Por favor, confirme que voc\u00ea deseja alterar o escopo de zona para o cluster especificado.<br>Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e desconectar\u00e1 o pool de armazenamento de todos os hosts que estavam conectados anteriormente ao armazenamento prim\u00e1rio e n\u00e3o fazem parte do cluster especificado.",
|
||||
"message.action.primary.storage.scope.zone": "Por favor, confirme que voc\u00ea deseja alterar o escopo de cluster para zona.<br>Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e conectar\u00e1 o pool de armazenamento a todos os hosts da zona executando o mesmo hypervisor definido no pool de armazenamento.",
|
||||
"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.credit.add.error.accountrequired": "Por favor, informe a conta",
|
||||
"message.action.quota.credit.add.error.domainidrequired": "Por favor, informe o dom\u00ednio",
|
||||
"message.action.quota.credit.add.error.valuerequired": "Por favor, informe a quantidade de cr\u00e9ditos",
|
||||
"message.action.quota.credit.add.success": "{credit} cr\u00e9ditos adicionados para a conta \"{account}\"",
|
||||
"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.",
|
||||
|
|
@ -3408,6 +3431,7 @@
|
|||
"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.quota.usage.resource.warn": "Recursos que s\u00e3o marcados como <untraceable> n\u00e3o possuem metadados constantes (se removido, o dado \u00e9 completamente deletado) e n\u00e3o podem ser retornados.",
|
||||
"message.read.accept.license.agreements": "Leia e aceite os termos dos contratos de licen\u00e7a.",
|
||||
"message.read.admin.guide.scaling.up": "Por favor leia a sess\u00e3o sobre escalonamento din\u00e2mico no guia do administrador antes de escalonar.",
|
||||
"message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.",
|
||||
|
|
@ -3442,6 +3466,7 @@
|
|||
"message.remove.sticky.policy.processing": "Removendo sticky policy",
|
||||
"message.remove.vpc": "Favor confirmar que voc\u00ea deseja remover a VPC",
|
||||
"message.request.failed": "Falha na solicita\u00e7\u00e3o",
|
||||
"message.request.no.data": "N\u00e3o h\u00e1 dados para mostrar.",
|
||||
"message.required.add.least.ip": "Por favor, adicionar pelo menos UM intervalo IP",
|
||||
"message.required.tagged.physical.network": "S\u00f3 pode haver uma rede f\u00edsica n\u00e3o marcada com tipo de tr\u00e1fego convidado.",
|
||||
"message.required.traffic.type": "Erro na configura\u00e7\u00e3o! Todos os tipos de tr\u00e1fego necess\u00e1rios devem ser adicionados e com m\u00faltiplas redes f\u00edsicas cada rede deve ter uma etiqueta.",
|
||||
|
|
@ -3773,6 +3798,8 @@
|
|||
"migrate.from": "Migrar de",
|
||||
"migrate.to": "Migrar para",
|
||||
"migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o",
|
||||
"placeholder.quota.credit.add.min_balance": "Saldo m\u00ednimo",
|
||||
"placeholder.quota.credit.add.value": "Quantidade de cr\u00e9ditos",
|
||||
"placeholder.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o",
|
||||
"placeholder.quota.tariff.description": "Descri\u00e7\u00e3o",
|
||||
"placeholder.quota.tariff.enddate": "Data de t\u00e9rmino",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const additionalGetAPICommandsList = [
|
|||
'quotatarifflist',
|
||||
'quotaisenabled',
|
||||
'quotastatement',
|
||||
'quotaemailtemplatelist',
|
||||
'verifyoauthcodeandgetuser'
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@
|
|||
<div v-else-if="['created', 'sent', 'lastannotated', 'collectiontime', 'lastboottime', 'lastserverstart', 'lastserverstop', 'removed', 'effectiveDate', 'endDate', 'startdate', 'enddate'].includes(item)">
|
||||
{{ $toLocaleDate(dataResource[item]) }}
|
||||
</div>
|
||||
<code-highlight v-else-if="['activationRule'].includes(item)" language="javascript">
|
||||
{{ dataResource[item] }}
|
||||
</code-highlight>
|
||||
<div style="white-space: pre-wrap;" v-else-if="$route.meta.name === 'quotatariff' && item === 'description'">{{ dataResource[item] }}</div>
|
||||
<div v-else-if="$route.meta.name === 'userdata' && item === 'userdata'">
|
||||
<div style="white-space: pre-wrap;"> {{ decodeUserData(dataResource.userdata)}} </div>
|
||||
|
|
@ -224,6 +227,8 @@ import VmwareData from './VmwareData'
|
|||
import ObjectListTable from '@/components/view/ObjectListTable'
|
||||
import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails'
|
||||
import { genericCompare } from '@/utils/sort'
|
||||
import CodeHighlight from 'vue-code-highlight/src/CodeHighlight.vue'
|
||||
import 'vue-code-highlight/themes/prism-okaidia.css'
|
||||
|
||||
export default {
|
||||
name: 'DetailsTab',
|
||||
|
|
@ -232,7 +237,8 @@ export default {
|
|||
HostInfo,
|
||||
VmwareData,
|
||||
ObjectListTable,
|
||||
ExternalConfigurationDetails
|
||||
ExternalConfigurationDetails,
|
||||
CodeHighlight
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@
|
|||
</span>
|
||||
<project-outlined v-else />
|
||||
<router-link v-if="!isStatic && resource.projectid" :to="{ path: '/project/' + resource.projectid }">{{ resource.project || resource.projectname || resource.projectid }}</router-link>
|
||||
<router-link v-else :to="{ path: '/project', query: { name: resource.projectname }}">{{ resource.projectname }}</router-link>
|
||||
<span v-else>{{ resource.projectname || resource.projectid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -887,6 +887,18 @@
|
|||
<span v-else>{{ resource.domain || resource.domainid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.currency">
|
||||
<div class="resource-detail-item__label">{{ $t('label.currency') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<span>{{ resource.currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.balance">
|
||||
<div class="resource-detail-item__label">{{ $t('label.quota.current.balance') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<span>{{ resource.balance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.payloadurl">
|
||||
<div class="resource-detail-item__label">{{ $t('label.payloadurl') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@
|
|||
</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'templatetype'">
|
||||
<span>{{ text }}</span>
|
||||
<router-link :to="{ path: $route.path + '/' + record.templatetype }">{{ text }}</router-link>
|
||||
</template>
|
||||
<template v-if="column.key === 'gpu'">
|
||||
<span v-if="record.gpucardname && record.vgpuprofilename">
|
||||
|
|
@ -681,20 +681,19 @@
|
|||
</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="text && !text.startsWith('PrjAcct-')">
|
||||
<router-link
|
||||
v-if="'quota' in record && $router.resolve(`${$route.path}/${record.account}`).matched[0].redirect !== '/exception/404'"
|
||||
:to="{ path: `${$route.path}/${record.account}`, query: { account: record.account, domainid: record.domainid, quota: true } }"
|
||||
>{{ text }}</router-link>
|
||||
<router-link
|
||||
:to="{ path: '/account/' + record.accountid }"
|
||||
v-else-if="record.accountid"
|
||||
>{{ text }}</router-link>
|
||||
<router-link
|
||||
:to="{ path: '/account', query: { name: record.account, domainid: record.domainid, dataView: true } }"
|
||||
v-else-if="$store.getters.userInfo.roletype !== 'User'"
|
||||
>{{ text }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
<template v-if="text">
|
||||
<template v-if="!text.startsWith('PrjAcct-')">
|
||||
<router-link
|
||||
v-if="$route.path.startsWith('/quotasummary') && $router.resolve(`${$route.path}/${record.accountid}`).matched[0].redirect !== '/exception/404'"
|
||||
:to="{ path: `${$route.path}/${record.accountid}` }">{{ text }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link
|
||||
v-if="$route.path.startsWith('/quotasummary') && $router.resolve(`${$route.path}/${record.accountid}`).matched[0].redirect !== '/exception/404'"
|
||||
:to="{ path: `${$route.path}/${record.accountid}` }">{{ (record.projectname || record.account).concat(' (').concat($t('label.project')).concat(')') }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'resource'">
|
||||
|
|
@ -1034,16 +1033,6 @@
|
|||
/>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template v-if="column.key === 'tariffActions'">
|
||||
<tooltip-button
|
||||
:tooltip="$t('label.edit')"
|
||||
v-if="editableValueKey !== record.key"
|
||||
:disabled="!('quotaTariffUpdate' in $store.getters.apis)"
|
||||
icon="edit-outlined"
|
||||
@onClick="editTariffValue(record)"
|
||||
/>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template v-if="column.key === 'scheduleActions'">
|
||||
<slot
|
||||
name="scheduleActions"
|
||||
|
|
@ -1225,8 +1214,8 @@ export default {
|
|||
'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp',
|
||||
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
|
||||
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets',
|
||||
'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile'
|
||||
].includes(this.$route.name)
|
||||
'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile',
|
||||
'quotatariff'].includes(this.$route.name)
|
||||
},
|
||||
getDateAtTimeZone (date, timezone) {
|
||||
return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
|
|
@ -1392,9 +1381,6 @@ export default {
|
|||
data.push(data.splice(index, 1)[0])
|
||||
this.updateOrder(data)
|
||||
},
|
||||
editTariffValue (record) {
|
||||
this.$emit('edit-tariff-action', true, record)
|
||||
},
|
||||
ipV6Address (text, record) {
|
||||
if (!record || !record.nic || record.nic.length === 0) {
|
||||
return ''
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// 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-button type="dashed" @click="action" class="w-100 mb-10 mt-10">
|
||||
<download-outlined />
|
||||
{{ $t(label) }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExportToCsvButton',
|
||||
props: {
|
||||
action: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'label.export.data.csv'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/common/common.scss';
|
||||
</style>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// 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>
|
||||
<bar
|
||||
:chart-options="chartOptions"
|
||||
:chart-data="chartData"
|
||||
:width="chartWidth"
|
||||
:height="chartHeight"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, TimeScale, LinearScale } from 'chart.js'
|
||||
import 'chartjs-adapter-moment'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, TimeScale, LinearScale)
|
||||
|
||||
export default {
|
||||
name: 'BarChart',
|
||||
components: { Bar },
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
chartWidth: {
|
||||
type: Number,
|
||||
default: 1024
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -217,7 +217,7 @@ export default {
|
|||
data: element.data.map(d => d.stat),
|
||||
hidden: this.hideLine(element.data.map(d => d.stat)),
|
||||
pointRadius: element.pointRadius,
|
||||
fill: 'origin'
|
||||
fill: element.fill || 'origin'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,29 +30,38 @@ export default {
|
|||
title: 'label.quota.summary',
|
||||
icon: 'bars-outlined',
|
||||
permission: ['quotaSummary'],
|
||||
columns: ['account',
|
||||
{
|
||||
state: (record) => record.state.toLowerCase()
|
||||
},
|
||||
{
|
||||
quotastate: (record) => record.quotaenabled ? 'Enabled' : 'Disabled'
|
||||
}, 'domain', 'currency', 'balance'
|
||||
],
|
||||
columnNames: ['account', 'accountstate', 'quotastate', 'domain', 'currency', 'currentbalance'],
|
||||
details: ['account', 'domain', 'state', 'currency', 'balance', 'quota', 'startdate', 'enddate'],
|
||||
component: shallowRef(() => import('@/views/plugins/quota/QuotaSummary.vue')),
|
||||
customParamHandler: (params, query) => { return { ...params, ignoreproject: true } },
|
||||
tabs: [
|
||||
{
|
||||
name: 'details',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
|
||||
name: 'consumption',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaUsageTab.vue')))
|
||||
},
|
||||
{
|
||||
name: 'quota.statement.quota',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaUsage.vue')))
|
||||
name: 'balance',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaBalanceTab.vue')))
|
||||
},
|
||||
{
|
||||
name: 'quota.statement.balance',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaBalance.vue')))
|
||||
name: 'credits',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaCreditTab.vue')))
|
||||
}
|
||||
],
|
||||
columns: [
|
||||
'account',
|
||||
{
|
||||
field: 'state',
|
||||
customTitle: 'accountState',
|
||||
state: (record) => record.accountremoved || (record.projectid && record.projectremoved) ? 'disabled' : 'enabled'
|
||||
},
|
||||
{
|
||||
field: 'quotastate',
|
||||
customTitle: 'quotaState',
|
||||
quotastate: (record) => !record.quotaenabled || record.accountremoved || (record.projectid && record.projectremoved) ? 'disabled' : 'enabled'
|
||||
},
|
||||
'domain',
|
||||
'currency',
|
||||
{
|
||||
field: 'balance',
|
||||
customTitle: 'quota.current.balance'
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
|
|
@ -61,16 +70,9 @@ export default {
|
|||
icon: 'plus-outlined',
|
||||
docHelp: 'plugins/quota.html#quota-credits',
|
||||
label: 'label.quota.add.credits',
|
||||
dataView: true,
|
||||
args: ['value', 'min_balance', 'quota_enforce'],
|
||||
mapping: {
|
||||
account: {
|
||||
value: (record) => { return record.account }
|
||||
},
|
||||
domainid: {
|
||||
value: (record) => { return record.domainid }
|
||||
}
|
||||
}
|
||||
listView: true,
|
||||
popup: true,
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/AddQuotaCredit.vue')))
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -83,7 +85,7 @@ export default {
|
|||
customParamHandler: (params, query) => {
|
||||
params.listall = false
|
||||
|
||||
if (['all', 'removed'].includes(query.filter) || params.id) {
|
||||
if (['all', 'removed'].includes(query.filter) || params.uuid) {
|
||||
params.listall = true
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +111,11 @@ export default {
|
|||
field: 'tariffValue',
|
||||
customTitle: 'quota.tariff.value'
|
||||
},
|
||||
{
|
||||
field: 'hasActivationRule',
|
||||
customTitle: 'quota.tariff.hasactivationrule',
|
||||
hasActivationRule: (record) => record.activationRule ? i18n.global.t('label.yes') : i18n.global.t('label.no')
|
||||
},
|
||||
{
|
||||
field: 'executionPosition',
|
||||
customTitle: 'quota.tariff.position',
|
||||
|
|
@ -145,7 +152,11 @@ export default {
|
|||
field: 'endDate',
|
||||
customTitle: 'end.date'
|
||||
},
|
||||
'removed'
|
||||
'removed',
|
||||
{
|
||||
field: 'activationRule',
|
||||
customTitle: 'quota.tariff.activationrule'
|
||||
}
|
||||
],
|
||||
filters: ['all', 'active', 'removed'],
|
||||
searchFilters: ['usagetype'],
|
||||
|
|
@ -173,13 +184,16 @@ export default {
|
|||
label: 'label.action.quota.tariff.remove',
|
||||
message: 'message.action.quota.tariff.remove',
|
||||
dataView: true,
|
||||
show: (record) => !record.removed
|
||||
show: (record) => !record.removed,
|
||||
groupAction: true,
|
||||
popup: true,
|
||||
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'quotaemailtemplate',
|
||||
title: 'label.templatetype',
|
||||
title: 'label.emailtemplate',
|
||||
icon: 'mail-outlined',
|
||||
permission: ['quotaEmailTemplateList'],
|
||||
columns: ['templatetype', 'templatesubject', 'templatebody'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.m-20-0 {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.dotted-underline {
|
||||
text-decoration: underline dotted;
|
||||
cursor: default;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// 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.
|
||||
|
||||
import { TIME_UNITS } from './units'
|
||||
|
||||
export const defaultDisplayFormats = {
|
||||
day: 'DD MMM YYYY',
|
||||
week: 'DD MMM YYYY',
|
||||
month: 'MMM YYYY',
|
||||
quarter: 'MMM YYYY',
|
||||
year: 'YYYY'
|
||||
}
|
||||
|
||||
export const getUnitToTimeCartesianAxis = (baseUnit, dataLength) => {
|
||||
const maxLabels = 15
|
||||
if (dataLength <= maxLabels) {
|
||||
return baseUnit
|
||||
}
|
||||
|
||||
const units = [
|
||||
'millisecond',
|
||||
'second',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'quarter',
|
||||
'year'
|
||||
]
|
||||
|
||||
let index = units.indexOf(baseUnit)
|
||||
|
||||
let unitToReturn = baseUnit
|
||||
if (index >= 0 && index < units.length) {
|
||||
let unitTime = 0
|
||||
for (index; index < units.length; index++) {
|
||||
unitTime = TIME_UNITS[units[index]]
|
||||
const nextUnitTime = TIME_UNITS[units[index + 1]]
|
||||
|
||||
if ((dataLength / (nextUnitTime / unitTime)) <= maxLabels) {
|
||||
return units[index + 1]
|
||||
}
|
||||
|
||||
unitToReturn = units[index]
|
||||
}
|
||||
}
|
||||
|
||||
return unitToReturn
|
||||
}
|
||||
|
||||
export const getChartColorObject = (hexColor = '#1890FF') => ({
|
||||
backgroundColor: hexColor.concat('80'),
|
||||
borderColor: hexColor,
|
||||
borderWidth: 1.5
|
||||
})
|
||||
|
|
@ -65,16 +65,21 @@ export function parseDateToDatePicker (value) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
timezoneoffset = getTimezoneOffset({ timezoneoffset, usebrowsertimezone })
|
||||
|
||||
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 getTimezoneOffset ({ timezoneoffset = store.getters.timezoneoffset, usebrowsertimezone = store.getters.usebrowsertimezone }) {
|
||||
if (!usebrowsertimezone) {
|
||||
return timezoneoffset
|
||||
}
|
||||
// Since GMT+530 is returned as -330 (mins to GMT)
|
||||
return new Date().getTimezoneOffset() / -60
|
||||
}
|
||||
|
||||
export function toLocaleDate ({ date, timezoneoffset = store.getters.timezoneoffset, usebrowsertimezone = store.getters.usebrowsertimezone, dateOnly = false, hourOnly = false }) {
|
||||
if (!date) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
// 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.
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export function exportDataToCsv ({ data = null, keys = null, headers = null, columnDelimiter = ',', lineDelimiter = '\n', fileName = 'data', dateFormat = undefined }) {
|
||||
if (data === null || !data.length || keys === null || !keys.filter(key => key !== null && key !== '').length) {
|
||||
return null
|
||||
}
|
||||
|
||||
let dataParsed = ''
|
||||
dataParsed += (headers || keys).join(columnDelimiter)
|
||||
dataParsed += lineDelimiter
|
||||
|
||||
data.forEach(item => {
|
||||
keys.forEach(key => {
|
||||
if (item[key] === undefined) {
|
||||
item[key] = ''
|
||||
}
|
||||
|
||||
if (typeof item[key] === 'string' && item[key].includes(columnDelimiter)) {
|
||||
dataParsed += `"${item[key]}"`
|
||||
} else if (dateFormat && dayjs.isDayjs(item[key])) {
|
||||
dataParsed += `"${item[key].format(dateFormat)}"`
|
||||
} else {
|
||||
dataParsed += item[key]
|
||||
}
|
||||
|
||||
dataParsed += columnDelimiter
|
||||
})
|
||||
dataParsed = dataParsed.slice(0, -1)
|
||||
dataParsed += lineDelimiter
|
||||
})
|
||||
|
||||
const hiddenElement = document.createElement('a')
|
||||
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(dataParsed)
|
||||
hiddenElement.target = '_blank'
|
||||
hiddenElement.download = `${fileName}.csv`
|
||||
hiddenElement.click()
|
||||
hiddenElement.remove()
|
||||
}
|
||||
|
|
@ -19,106 +19,135 @@
|
|||
export const QUOTA_TYPES = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'RUNNING_VM'
|
||||
type: 'RUNNING_VM',
|
||||
chartColor: '#1890ff'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ALLOCATED_VM'
|
||||
type: 'ALLOCATED_VM',
|
||||
chartColor: '#fadb14'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'IP_ADDRESS'
|
||||
type: 'IP_ADDRESS',
|
||||
chartColor: '#ffd6e7'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'NETWORK_BYTES_SENT'
|
||||
type: 'NETWORK_BYTES_SENT',
|
||||
chartColor: '#adc6ff'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'NETWORK_BYTES_RECEIVED'
|
||||
type: 'NETWORK_BYTES_RECEIVED',
|
||||
chartColor: '#10239e'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'VOLUME'
|
||||
type: 'VOLUME',
|
||||
chartColor: '#722ed1'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: 'TEMPLATE'
|
||||
type: 'TEMPLATE',
|
||||
chartColor: '#08979c'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: 'ISO'
|
||||
type: 'ISO',
|
||||
chartColor: '#87e8de'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
type: 'SNAPSHOT'
|
||||
type: 'SNAPSHOT',
|
||||
chartColor: '#f5222d'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
type: 'SECURITY_GROUP'
|
||||
type: 'SECURITY_GROUP',
|
||||
chartColor: '#d46b08'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
type: 'LOAD_BALANCER_POLICY'
|
||||
type: 'LOAD_BALANCER_POLICY',
|
||||
chartColor: '#ffd666'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
type: 'PORT_FORWARDING_RULE'
|
||||
type: 'PORT_FORWARDING_RULE',
|
||||
chartColor: '#7cb305'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
type: 'NETWORK_OFFERING'
|
||||
type: 'NETWORK_OFFERING',
|
||||
chartColor: '#ffbb96'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
type: 'VPN_USERS'
|
||||
type: 'VPN_USERS',
|
||||
chartColor: '#95de64'
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
type: 'VM_DISK_IO_READ'
|
||||
type: 'VM_DISK_IO_READ',
|
||||
chartColor: '#ffe7ba'
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
type: 'VM_DISK_IO_WRITE'
|
||||
type: 'VM_DISK_IO_WRITE',
|
||||
chartColor: '#5b8c00'
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
type: 'VM_DISK_BYTES_READ'
|
||||
type: 'VM_DISK_BYTES_READ',
|
||||
chartColor: '#0050b3'
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
type: 'VM_DISK_BYTES_WRITE'
|
||||
type: 'VM_DISK_BYTES_WRITE',
|
||||
chartColor: '#520339'
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
type: 'VM_SNAPSHOT'
|
||||
type: 'VM_SNAPSHOT',
|
||||
chartColor: '#9e1068'
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
type: 'VOLUME_SECONDARY'
|
||||
type: 'VOLUME_SECONDARY',
|
||||
chartColor: '#061178'
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
type: 'VM_SNAPSHOT_ON_PRIMARY'
|
||||
type: 'VM_SNAPSHOT_ON_PRIMARY',
|
||||
chartColor: '#ad2102'
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
type: 'BACKUP'
|
||||
type: 'BACKUP',
|
||||
chartColor: '#00474f'
|
||||
},
|
||||
{
|
||||
id: 29,
|
||||
type: 'BUCKET'
|
||||
type: 'BUCKET',
|
||||
chartColor: '#13a8a8'
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
type: 'NETWORK'
|
||||
type: 'NETWORK',
|
||||
chartColor: '#c75314'
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
type: 'VPC'
|
||||
type: 'VPC',
|
||||
chartColor: '#018391'
|
||||
}
|
||||
]
|
||||
|
||||
export const getQuotaTypes = () => {
|
||||
return QUOTA_TYPES.sort((a, b) => a.type.localeCompare(b.type))
|
||||
}
|
||||
|
||||
export const getQuotaTypeByName = (type) => {
|
||||
return QUOTA_TYPES.find(quotaType => quotaType.type === type)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
export const TIME_UNITS = {
|
||||
millisecond: 1,
|
||||
second: 1000,
|
||||
minute: 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
week: 7 * 24 * 60 * 60 * 1000,
|
||||
month: 30 * 24 * 60 * 60 * 1000,
|
||||
quarter: 91 * 24 * 60 * 60 * 1000,
|
||||
year: 365 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
|
@ -558,7 +558,7 @@
|
|||
<div v-if="dataView">
|
||||
<slot
|
||||
name="resource"
|
||||
v-if="$route.path.startsWith('/quotasummary') || $route.path.startsWith('/publicip')"
|
||||
v-if="$route.path.startsWith('/publicip')"
|
||||
></slot>
|
||||
<resource-view
|
||||
v-else
|
||||
|
|
@ -1067,7 +1067,7 @@ export default {
|
|||
const customRender = {}
|
||||
for (var columnKey of this.columnKeys) {
|
||||
let key = columnKey
|
||||
let title = columnKey === 'cidr' && this.columnKeys.includes('ip6cidr') ? 'ipv4.cidr' : columnKey
|
||||
let title = columnKey === 'cidr' && this.columnKeys.includes('ip6cidr') ? 'ipv4.cidr' : key
|
||||
if (typeof columnKey === 'object') {
|
||||
if ('customTitle' in columnKey && 'field' in columnKey) {
|
||||
key = columnKey.field
|
||||
|
|
@ -1075,7 +1075,7 @@ export default {
|
|||
customRender[key] = columnKey[key]
|
||||
} else {
|
||||
key = Object.keys(columnKey)[0]
|
||||
title = Object.keys(columnKey)[0]
|
||||
title = (typeof title === 'object') ? key : title
|
||||
customRender[key] = columnKey[key]
|
||||
}
|
||||
}
|
||||
|
|
@ -1120,6 +1120,13 @@ export default {
|
|||
params.details = 'group,nics,secgrp,tmpl,servoff,diskoff,iso,volume,affgrp,backoff'
|
||||
}
|
||||
|
||||
if (this.apiName === 'quotaTariffList' && !('quotaTariffCreate' in store.getters.apis || 'quotaTariffUpdate' in store.getters.apis)) {
|
||||
const index = this.columns.findIndex(col => col.dataIndex === 'hasActivationRule')
|
||||
if (index >= 0) {
|
||||
this.columns.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
if (this.$route.path.startsWith('/cniconfiguration')) {
|
||||
params.forcks = true
|
||||
|
|
@ -1160,6 +1167,10 @@ export default {
|
|||
if (this.$route.path.startsWith('/tungstenfirewallpolicy/')) {
|
||||
params.firewallpolicyuuid = this.$route.params.id
|
||||
}
|
||||
if (this.apiName === 'quotaSummary' && params.id) {
|
||||
params.accountid = params.id
|
||||
delete params.id
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$store.getters.listAllProjects && !this.projectView) {
|
||||
|
|
@ -1206,7 +1217,7 @@ export default {
|
|||
break
|
||||
}
|
||||
|
||||
if ('id' in this.$route.params && this.$route.params.id !== params.id) {
|
||||
if ('id' in this.$route.params && !(this.$route.params.id === params.id || this.apiName === 'quotaSummary' && this.$route.params.id === params.accountid)) {
|
||||
console.log('DEBUG - Discarding API response as its `id` does not match the uuid on the browser path')
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
// 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">
|
||||
<ownership-selection @fetch-owner="fetchOwnerOptions" />
|
||||
<a-form-item ref="value" name="value">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.value')" :tooltip="apiParams.value.description"/>
|
||||
</template>
|
||||
<a-input-number
|
||||
v-model:value="form.value"
|
||||
:placeholder="$t('placeholder.quota.credit.add.value')" />
|
||||
</a-form-item>
|
||||
<a-form-item ref="min_balance" name="min_balance">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.min_balance')" :tooltip="apiParams.min_balance.description"/>
|
||||
</template>
|
||||
<a-input-number
|
||||
v-model:value="form.min_balance"
|
||||
:placeholder="$t('placeholder.quota.credit.add.min_balance')" />
|
||||
</a-form-item>
|
||||
<a-form-item ref="quota_enforce" name="quota_enforce">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.quota.enforce')" :tooltip="apiParams.quota_enforce.description"/>
|
||||
</template>
|
||||
<a-switch
|
||||
v-model:checked="form.quota_enforce" />
|
||||
</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 { getAPI } from '@/api'
|
||||
import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { mixinForm } from '@/utils/mixin'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'AddQuotaCredit',
|
||||
mixins: [mixinForm],
|
||||
components: {
|
||||
OwnershipSelection,
|
||||
TooltipLabel
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
domainList: [],
|
||||
accountList: [],
|
||||
domainId: undefined,
|
||||
domainLoading: false,
|
||||
domainError: false,
|
||||
owner: {
|
||||
projectid: store.getters.project?.id,
|
||||
domainid: store.getters.project?.id ? null : store.getters.userInfo.domainid,
|
||||
account: store.getters.project?.id ? null : store.getters.userInfo.account,
|
||||
name: store.getters.project?.id ? store.getters.project.name : store.getters.userInfo.account
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: ['parentFetchData'],
|
||||
beforeCreate () {
|
||||
this.apiParams = this.$getApiParams('quotaCredits')
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.rules = reactive({
|
||||
value: [{ required: true, message: this.$t('message.action.quota.credit.add.error.valuerequired') }]
|
||||
})
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
|
||||
this.formRef.value.validate().then(() => {
|
||||
const formRaw = toRaw(this.form)
|
||||
const values = this.handleRemoveFields(formRaw)
|
||||
values.ignoreproject = true
|
||||
|
||||
if (this.owner.projectid) {
|
||||
values.projectid = this.owner.projectid
|
||||
} else {
|
||||
values.account = this.owner.account
|
||||
values.domainid = this.owner.domainid
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
getAPI('quotaCredits', values).then(response => {
|
||||
this.$message.success(this.$t('message.action.quota.credit.add.success',
|
||||
{ credit: response.quotacreditsresponse.quotacredits.credit, account: this.owner.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')
|
||||
},
|
||||
fetchOwnerOptions (OwnerOptions) {
|
||||
this.owner = {}
|
||||
if (OwnerOptions.selectedAccountType === 'Account') {
|
||||
if (!OwnerOptions.selectedAccount) {
|
||||
return
|
||||
}
|
||||
this.owner.account = OwnerOptions.selectedAccount
|
||||
this.owner.domainid = OwnerOptions.selectedDomain
|
||||
this.owner.name = OwnerOptions.selectedAccount
|
||||
} else if (OwnerOptions.selectedAccountType === 'Project') {
|
||||
if (!OwnerOptions.selectedProject) {
|
||||
return
|
||||
}
|
||||
this.owner.projectid = OwnerOptions.selectedProject
|
||||
this.owner.name = OwnerOptions.projects.find(p => p.id === OwnerOptions.selectedProject).name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/objects/form.scss';
|
||||
</style>
|
||||
|
|
@ -62,7 +62,6 @@
|
|||
<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>
|
||||
|
|
@ -85,7 +84,6 @@
|
|||
<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>
|
||||
|
|
@ -94,7 +92,6 @@
|
|||
<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')"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
<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>
|
||||
|
|
@ -61,7 +60,6 @@
|
|||
<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>
|
||||
|
|
@ -70,7 +68,6 @@
|
|||
<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')"
|
||||
|
|
|
|||
|
|
@ -1,145 +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>
|
||||
<a-modal
|
||||
v-if="showAction"
|
||||
style="top: 20px;"
|
||||
centered
|
||||
:confirmLoading="loading"
|
||||
:title="$t('label.quota.configuration')"
|
||||
:closable="true"
|
||||
:maskClosable="false"
|
||||
:visible="showAction"
|
||||
:footer="null"
|
||||
@cancel="onClose"
|
||||
>
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="submitTariff"
|
||||
v-ctrl-enter="submitTariff"
|
||||
>
|
||||
<a-form-item name="value" ref="value" :label="$t('label.quota.value')">
|
||||
<a-input
|
||||
v-focus="true"
|
||||
v-model:value="form.value"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="startdate" ref="startdate" :label="$t('label.quota.tariff.effectivedate')">
|
||||
<a-date-picker
|
||||
:disabledDate="disabledDate"
|
||||
style="width: 100%"
|
||||
v-model:value="form.startdate"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="onClose">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" @click="submitTariff">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'EditTariffValueWizard',
|
||||
props: {
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
pattern: 'YYYY-MM-DD'
|
||||
}
|
||||
},
|
||||
inject: ['parentFetchData'],
|
||||
created () {
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
value: this.resource.tariffValue
|
||||
})
|
||||
this.rules = reactive({
|
||||
value: [{ required: true, message: this.$t('message.error.required.input') }],
|
||||
startdate: [{ type: 'object', required: true, message: this.$t('message.error.date') }]
|
||||
})
|
||||
},
|
||||
onClose () {
|
||||
this.$emit('edit-tariff-action', false)
|
||||
},
|
||||
submitTariff (e) {
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const values = toRaw(this.form)
|
||||
|
||||
const params = {}
|
||||
params.usageType = this.resource.usageType
|
||||
params.value = values.value
|
||||
params.startdate = values.startdate.format(this.pattern)
|
||||
|
||||
this.loading = true
|
||||
|
||||
postAPI('quotaTariffUpdate', params).then(json => {
|
||||
const tariffResponse = json.quotatariffupdateresponse.quotatariff || {}
|
||||
if (Object.keys(tariffResponse).length > 0) {
|
||||
const effectiveDate = moment(tariffResponse.effectiveDate).format(this.pattern)
|
||||
const query = this.$route.query
|
||||
if (query.startdate !== effectiveDate) {
|
||||
this.$router.replace({ path: 'quotatariff', query: { startdate: effectiveDate } })
|
||||
}
|
||||
this.parentFetchData()
|
||||
}
|
||||
this.$message.success(`${this.$t('message.setting.updated')} ${this.resource.description}`)
|
||||
this.onClose()
|
||||
}).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
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
disabledDate (current) {
|
||||
return current && current < moment().endOf('day')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// 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>
|
||||
<a-modal
|
||||
v-model:visible="showFilterQuotaDataByPeriodModal"
|
||||
:title="$t('label.quota.select.period')"
|
||||
:maskClosable="false"
|
||||
:footer="null">
|
||||
<a-form
|
||||
class="form-layout"
|
||||
:model="form"
|
||||
@finish="handleSubmit"
|
||||
v-ctrl-enter="handleSubmit">
|
||||
<a-form-item ref="dates" name="dates" style="width: 100%">
|
||||
<a-range-picker
|
||||
class="w-100"
|
||||
v-model:value="form.dates"
|
||||
show-time />
|
||||
</a-form-item>
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeFilterQuotaDataByPeriodModal">{{ this.$t('label.cancel') }}</a-button>
|
||||
<a-button ref="submit" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<div class="chart-row">
|
||||
<a-space direction="vertical">
|
||||
<div>
|
||||
<a-radio-group
|
||||
v-model:value="periodSelected"
|
||||
buttonStyle="solid"
|
||||
@change="handlePeriodChange">
|
||||
<a-radio-button value="day">
|
||||
{{ $t('label.quota.period.today') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="week">
|
||||
{{ $t('label.quota.period.this.week') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="month">
|
||||
{{ $t('label.quota.period.this.month') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="lastmonth">
|
||||
{{ $t('label.quota.period.last.month') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="year">
|
||||
{{ $t('label.quota.period.this.year') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="lastyear">
|
||||
{{ $t('label.quota.period.last.year') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="custom" @click="openFilterQuotaDataByPeriodModal">
|
||||
{{ $t('label.quota.period.custom') }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</a-space>
|
||||
<div class="mt-10">
|
||||
<filter-outlined style="margin-right: 5px"/>
|
||||
<span v-html="getPeriodToString()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { dayjs, parseDayJsObject, toLocaleDate } from '@/utils/date'
|
||||
import { reactive, toRaw } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'FilterQuotaDataByPeriodView',
|
||||
data () {
|
||||
return {
|
||||
periodSelected: 'month',
|
||||
showFilterQuotaDataByPeriodModal: false,
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
this.handlePeriodChange()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.form = reactive({ dates: [this.startDate, this.endDate] })
|
||||
},
|
||||
handlePeriodChange () {
|
||||
let end = dayjs()
|
||||
let start
|
||||
switch (this.periodSelected) {
|
||||
case 'day':
|
||||
case 'week':
|
||||
case 'month':
|
||||
case 'year':
|
||||
start = dayjs().startOf(this.periodSelected)
|
||||
break
|
||||
case 'lastmonth': {
|
||||
const lastMonth = dayjs().subtract(1, 'months')
|
||||
start = dayjs(lastMonth).startOf('month')
|
||||
end = dayjs(lastMonth).endOf('month')
|
||||
break
|
||||
}
|
||||
case 'lastyear': {
|
||||
const lastYear = dayjs().subtract(1, 'years')
|
||||
start = dayjs(lastYear).startOf('year')
|
||||
end = dayjs(lastYear).endOf('year')
|
||||
break
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
this.triggerFetchData([start, end])
|
||||
},
|
||||
openFilterQuotaDataByPeriodModal () {
|
||||
this.showFilterQuotaDataByPeriodModal = true
|
||||
},
|
||||
closeFilterQuotaDataByPeriodModal () {
|
||||
this.showFilterQuotaDataByPeriodModal = false
|
||||
},
|
||||
getPeriodToString () {
|
||||
return this.$t('label.quota.filter.period', {
|
||||
startDate: toLocaleDate({ date: parseDayJsObject({ value: this.startDate, keepMoment: true, format: false }) }),
|
||||
endDate: toLocaleDate({ date: parseDayJsObject({ value: this.endDate, keepMoment: true, format: false }) })
|
||||
})
|
||||
},
|
||||
handleSubmit () {
|
||||
const formRaw = toRaw(this.form)
|
||||
this.triggerFetchData(formRaw.dates)
|
||||
},
|
||||
triggerFetchData (values) {
|
||||
this.startDate = values[0]
|
||||
this.endDate = values[1]
|
||||
this.initForm()
|
||||
this.closeFilterQuotaDataByPeriodModal()
|
||||
this.$emit('fetchData', this.startDate, this.endDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/common/common.scss';
|
||||
</style>
|
||||
|
|
@ -1,173 +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>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:rowKey="record => record.name"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }"
|
||||
>
|
||||
<template #bodyCell="{ column, text }">
|
||||
<template v-if="column.key === 'quota'">
|
||||
<span v-if="text!==null">{{ `${currency} ${text}` }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'credit'">
|
||||
<span v-if="text!==null">{{ `${currency} ${text}` }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'QuotaBalance',
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
tab: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
pattern: 'YYYY-MM-DD',
|
||||
currency: '',
|
||||
dataSource: [],
|
||||
account: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return [
|
||||
{
|
||||
key: 'date',
|
||||
title: this.$t('label.date'),
|
||||
dataIndex: 'date',
|
||||
width: 'calc(100% / 3)'
|
||||
},
|
||||
{
|
||||
key: 'quota',
|
||||
title: this.$t('label.quota.value'),
|
||||
dataIndex: 'quota',
|
||||
width: 'calc(100% / 3)'
|
||||
},
|
||||
{
|
||||
key: 'credit',
|
||||
title: this.$t('label.credit'),
|
||||
dataIndex: 'credit',
|
||||
width: 'calc(100% / 3)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tab () {
|
||||
if (this.tab === 'quota.statement.balance') {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
this.dataSource = []
|
||||
this.loading = true
|
||||
this.account = this.$route.query && this.$route.query.account ? this.$route.query.account : null
|
||||
|
||||
try {
|
||||
const resource = await this.fetchResource()
|
||||
const quotaBalance = await this.quotaBalance(resource)
|
||||
this.currency = quotaBalance.currency
|
||||
this.dataSource = await this.createDataSource(quotaBalance)
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
createDataSource (quotaBalance) {
|
||||
const dataSource = []
|
||||
const credits = quotaBalance.credits || []
|
||||
|
||||
dataSource.push({
|
||||
date: moment(quotaBalance.enddate).format(this.pattern),
|
||||
quota: quotaBalance.endquota,
|
||||
credit: null
|
||||
})
|
||||
dataSource.push({
|
||||
date: moment(quotaBalance.startdate).format(this.pattern),
|
||||
quota: quotaBalance.startquota,
|
||||
credit: null
|
||||
})
|
||||
credits.map(item => {
|
||||
dataSource.push({
|
||||
date: moment(item.updated_on).format(this.pattern),
|
||||
quota: null,
|
||||
credit: item.credits
|
||||
})
|
||||
})
|
||||
|
||||
return dataSource
|
||||
},
|
||||
fetchResource () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {}
|
||||
params.domainid = this.resource.domainid
|
||||
params.account = this.account
|
||||
|
||||
getAPI('quotaBalance', params).then(json => {
|
||||
const quotaBalance = json.quotabalanceresponse.balance || {}
|
||||
resolve(quotaBalance)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
quotaBalance (resource) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {}
|
||||
params.domainid = this.resource.domainid
|
||||
params.account = this.account
|
||||
params.startdate = moment(this.resource.startdate).format(this.pattern)
|
||||
params.enddate = moment(resource.startdate).format(this.pattern)
|
||||
|
||||
getAPI('quotaBalance', params).then(json => {
|
||||
const quotaBalance = json.quotabalanceresponse.balance || {}
|
||||
resolve(quotaBalance)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// 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>
|
||||
<filter-quota-data-by-period-view @fetchData="fetchData"/>
|
||||
|
||||
<div v-if="dataSource.length > 0">
|
||||
<export-to-csv-button :action="exportDataToCsv" />
|
||||
<bar-chart :chart-options="getBalancesChartOptions()" :chart-data="getBalancesChartData()"/>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:rowKey="record => record.date"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }">
|
||||
<template #title>
|
||||
{{ $t('label.currency') }}: <b>{{ currency }}</b>
|
||||
</template>
|
||||
<template #date="{ text }">
|
||||
{{ text }}
|
||||
</template>
|
||||
<template #lastBalanceHour="{ text }">
|
||||
{{ text }}
|
||||
</template>
|
||||
<template #balance="{ text }">
|
||||
{{ parseFloat(text).toFixed(2) }}
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import BarChart from '@/components/view/charts/BarChart.vue'
|
||||
import * as dateUtils from '@/utils/date'
|
||||
import * as exportUtils from '@/utils/export'
|
||||
import FilterQuotaDataByPeriodView from './FilterQuotaDataByPeriodView.vue'
|
||||
import ExportToCsvButton from '@/components/view/buttons/ExportToCsvButton.vue'
|
||||
import * as chartUtils from '@/utils/chart'
|
||||
|
||||
export default {
|
||||
name: 'QuotaBalance',
|
||||
components: {
|
||||
FilterQuotaDataByPeriodView,
|
||||
BarChart,
|
||||
ExportToCsvButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
currency: '',
|
||||
dataSource: [],
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return [
|
||||
{
|
||||
title: this.$t('label.date'),
|
||||
dataIndex: 'date',
|
||||
width: 'calc(100% / 3)',
|
||||
slots: { customRender: 'date' },
|
||||
sorter: (a, b) => a.lastBalance.localeCompare(b.lastBalance),
|
||||
defaultSortOrder: 'descend'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.quota.last.balance'),
|
||||
dataIndex: 'lastBalanceHour',
|
||||
width: 'calc(100% / 3)',
|
||||
slots: { customRender: 'lastBalanceHour' },
|
||||
sorter: (a, b) => a.lastBalance.localeCompare(b.lastBalance),
|
||||
defaultSortOrder: 'descend'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.balance'),
|
||||
dataIndex: 'balance',
|
||||
width: 'calc(100% / 3)',
|
||||
slots: { customRender: 'balance' },
|
||||
sorter: (a, b) => a.balance - b.balance
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchData (startDate, endDate) {
|
||||
if (this.loading) return
|
||||
|
||||
this.startDate = dateUtils.parseDayJsObject({ value: startDate })
|
||||
this.endDate = dateUtils.parseDayJsObject({ value: endDate })
|
||||
this.dataSource = []
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const data = await this.getQuotaBalance() || {}
|
||||
this.currency = data.currency
|
||||
this.dataSource = this.getLastBalanceOfEachDate(data.balances)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async getQuotaBalance () {
|
||||
const params = {
|
||||
accountid: this.$route.params?.id,
|
||||
startdate: this.startDate,
|
||||
enddate: this.endDate
|
||||
}
|
||||
|
||||
return await getAPI('quotaBalance', params)
|
||||
.then(json => json.quotabalanceresponse.balance)
|
||||
.catch(error => { error && this.$notification.info({ message: this.$t('message.request.no.data') }) })
|
||||
},
|
||||
getLastBalanceOfEachDate (data) {
|
||||
if (!data) return []
|
||||
|
||||
return data.reduce((reduced, currentItem) => {
|
||||
const lastBalance = dateUtils.parseDayJsObject({ value: currentItem.date, keepMoment: false })
|
||||
const balance = currentItem.balance
|
||||
const date = dateUtils.toLocaleDate({ date: lastBalance, dateOnly: true })
|
||||
const lastBalanceHour = dateUtils.toLocaleDate({ date: lastBalance, hourOnly: true })
|
||||
const found = reduced.find(item => item.date === date)
|
||||
|
||||
if (!found) {
|
||||
reduced.push({ lastBalance, balance, date, lastBalanceHour })
|
||||
} else {
|
||||
if (found.lastBalance < lastBalance) {
|
||||
found.balance = balance
|
||||
found.lastBalance = lastBalance
|
||||
found.date = date
|
||||
found.lastBalanceHour = lastBalanceHour
|
||||
}
|
||||
}
|
||||
return reduced
|
||||
}, [])
|
||||
},
|
||||
exportDataToCsv () {
|
||||
exportUtils.exportDataToCsv({
|
||||
data: this.dataSource,
|
||||
headers: ['date', 'balance'],
|
||||
keys: ['lastBalance', 'balance'],
|
||||
fileName: `quota-balances-of-user-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
|
||||
})
|
||||
},
|
||||
getBalancesChartData () {
|
||||
const datasets = []
|
||||
|
||||
datasets.push({
|
||||
label: this.$t('label.balance'),
|
||||
data: this.dataSource.map(row => row.balance),
|
||||
...chartUtils.getChartColorObject()
|
||||
})
|
||||
|
||||
return {
|
||||
labels: this.dataSource.map(row => row.date),
|
||||
datasets
|
||||
}
|
||||
},
|
||||
getBalancesChartOptions () {
|
||||
return {
|
||||
scales: {
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: chartUtils.getUnitToTimeCartesianAxis('day', this.dataSource.length),
|
||||
displayFormats: chartUtils.defaultDisplayFormats
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: (tooltipItem) => dateUtils.dayjs(tooltipItem[0].label).format(chartUtils.defaultDisplayFormats.day),
|
||||
label: (tooltipItem) => parseFloat(tooltipItem.raw).toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/common/common.scss';
|
||||
</style>
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
// 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>
|
||||
<filter-quota-data-by-period-view @fetchData="fetchData"/>
|
||||
|
||||
<div v-if="dataSource.length > 0">
|
||||
<export-to-csv-button :action="exportDataToCsv" />
|
||||
<bar-chart :chart-options="getCreditsChartOptions()" :chart-data="getCreditsChartData()"/>
|
||||
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:rowKey="record => record.creditedon"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }">
|
||||
<template #title>
|
||||
{{ $t('label.currency') }}: <b>{{ currency }}</b>
|
||||
</template>
|
||||
<template #creditedOn="{ text }">
|
||||
{{ $toLocaleDate(text) }}
|
||||
</template>
|
||||
<template #credit="{ text }">
|
||||
{{ parseFloat(text).toFixed(2) }}
|
||||
</template>
|
||||
<template #creditor="{ text, record }">
|
||||
<router-link :to="{ path: '/accountuser/' + record.creditoruserid }">{{ text }}</router-link>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import BarChart from '@/components/view/charts/BarChart.vue'
|
||||
import * as dateUtils from '@/utils/date'
|
||||
import * as exportUtils from '@/utils/export'
|
||||
import FilterQuotaDataByPeriodView from './FilterQuotaDataByPeriodView.vue'
|
||||
import ExportToCsvButton from '@/components/view/buttons/ExportToCsvButton.vue'
|
||||
import * as chartUtils from '@/utils/chart'
|
||||
|
||||
export default {
|
||||
name: 'QuotaCreditTab',
|
||||
components: {
|
||||
FilterQuotaDataByPeriodView,
|
||||
BarChart,
|
||||
ExportToCsvButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
currency: '',
|
||||
dataSource: [],
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return [
|
||||
{
|
||||
title: this.$t('label.date'),
|
||||
dataIndex: 'creditedon',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.creditedon.localeCompare(b.creditedon),
|
||||
defaultSortOrder: 'descend',
|
||||
slots: { customRender: 'creditedOn' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.credit'),
|
||||
dataIndex: 'credit',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.credit - b.credit,
|
||||
slots: { customRender: 'credit' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.creditor'),
|
||||
dataIndex: 'creditorusername',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.creditorusername.localeCompare(b.creditorusername),
|
||||
slots: { customRender: 'creditor' }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchData (startDate, endDate) {
|
||||
if (this.loading) return
|
||||
|
||||
this.startDate = dateUtils.parseDayJsObject({ value: startDate })
|
||||
this.endDate = dateUtils.parseDayJsObject({ value: endDate })
|
||||
this.dataSource = []
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const data = await this.getQuotaCreditsList()
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
this.currency = data[0]?.currency
|
||||
this.dataSource = data.map(row => ({
|
||||
...row,
|
||||
date: dateUtils.parseDayJsObject({ value: row.creditedon, keepMoment: false })
|
||||
}))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async getQuotaCreditsList () {
|
||||
const params = {
|
||||
accountid: this.$route.params?.id,
|
||||
startdate: this.startDate,
|
||||
enddate: this.endDate
|
||||
}
|
||||
|
||||
return await getAPI('quotaCreditsList', params)
|
||||
.then(json => json.quotacreditslistresponse.credit || [])
|
||||
.catch(error => { error && this.$notification.info({ message: this.$t('message.request.no.data') }) })
|
||||
},
|
||||
exportDataToCsv () {
|
||||
exportUtils.exportDataToCsv({
|
||||
data: this.dataSource,
|
||||
keys: ['date', 'credit', 'creditorusername'],
|
||||
fileName: `credits-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
|
||||
})
|
||||
},
|
||||
getCreditsChartData () {
|
||||
const datasets = []
|
||||
|
||||
const data = []
|
||||
const res = {}
|
||||
this.dataSource.map(value => {
|
||||
const date = this.$toLocalDate(value.creditedon).split('T')[0]
|
||||
|
||||
if (!res[date]) {
|
||||
res[date] = { date, credit: 0 }
|
||||
data.push(res[date])
|
||||
}
|
||||
|
||||
res[date].credit += value.credit
|
||||
return res
|
||||
})
|
||||
|
||||
datasets.push({
|
||||
label: this.$t('label.credit'),
|
||||
data: data.map(row => row.credit),
|
||||
...chartUtils.getChartColorObject()
|
||||
})
|
||||
|
||||
return {
|
||||
labels: data.map(row => row.date),
|
||||
datasets
|
||||
}
|
||||
},
|
||||
getCreditsChartOptions () {
|
||||
return {
|
||||
scales: {
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: chartUtils.getUnitToTimeCartesianAxis('day', this.dataSource.length),
|
||||
displayFormats: chartUtils.defaultDisplayFormats
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: (tooltipItem) => dateUtils.dayjs(tooltipItem[0].label).format(chartUtils.defaultDisplayFormats.day),
|
||||
label: (tooltipItem) => parseFloat(tooltipItem.raw).toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/common/common.scss';
|
||||
</style>
|
||||
|
|
@ -1,65 +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
|
||||
@change-resource="changeResource"/>
|
||||
<quota-summary-resource
|
||||
v-if="isSummaryResource"
|
||||
:resource="resource"
|
||||
:tabs="$route.meta.tabs"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AutogenView from '@/views/AutogenView.vue'
|
||||
import QuotaSummaryResource from '@/views/plugins/quota/QuotaSummaryResource'
|
||||
|
||||
export default {
|
||||
name: 'QuotaSummary',
|
||||
components: {
|
||||
AutogenView,
|
||||
QuotaSummaryResource
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resource: {}
|
||||
}
|
||||
},
|
||||
provide: function () {
|
||||
return {
|
||||
parentChangeResource: this.changeResource
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSummaryResource () {
|
||||
if (this.$route.path.startsWith('/quotasummary')) {
|
||||
if (this.$route.query && 'quota' in this.$route.query && this.$route.query.quota) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeResource (resource) {
|
||||
this.resource = resource
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,98 +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>
|
||||
<resource-view
|
||||
:loading="loading"
|
||||
:resource="quotaResource"
|
||||
:tabs="tabs"
|
||||
:historyTab="activeTab"
|
||||
@onTabChange="(tab) => { activeTab = tab }"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import moment from 'moment'
|
||||
|
||||
import ResourceView from '@/components/view/ResourceView'
|
||||
|
||||
export default {
|
||||
name: 'QuotaSummaryResource',
|
||||
components: {
|
||||
ResourceView
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
quotaResource: {},
|
||||
networkService: null,
|
||||
pattern: 'YYYY-MM-DD',
|
||||
activeTab: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
resource: {
|
||||
deep: true,
|
||||
handler () {
|
||||
if (Object.keys(this.resource).length === 0) {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: ['parentChangeResource'],
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = {}
|
||||
if (Object.keys(this.$route.query).length > 0) {
|
||||
Object.assign(params, this.$route.query)
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
getAPI('quotaBalance', params).then(json => {
|
||||
const quotaBalance = json.quotabalanceresponse.balance || {}
|
||||
if (Object.keys(quotaBalance).length > 0) {
|
||||
quotaBalance.currency = `${quotaBalance.currency} ${quotaBalance.startquota}`
|
||||
quotaBalance.startdate = moment(quotaBalance.startdate).format(this.pattern)
|
||||
quotaBalance.account = this.$route.params.id ? this.$route.params.id : null
|
||||
quotaBalance.domainid = this.$route.query.domainid ? this.$route.query.domainid : null
|
||||
}
|
||||
this.quotaResource = Object.assign({}, this.quotaResource, quotaBalance)
|
||||
this.parentChangeResource(this.quotaResource)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -1,158 +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>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:rowKey="record => record.name"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }"
|
||||
>
|
||||
<template #bodyCell="{ column, text }">
|
||||
<template v-if="column.key === 'quota'">
|
||||
<span v-if="text!==undefined">{{ `${currency} ${text}` }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'QuotaUsage',
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
tab: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
dataSource: [],
|
||||
pattern: 'YYYY-MM-DD',
|
||||
currency: '',
|
||||
totalQuota: 0,
|
||||
account: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
title: this.$t('label.quota.type.name'),
|
||||
dataIndex: 'name',
|
||||
width: 'calc(100% / 3)'
|
||||
},
|
||||
{
|
||||
key: 'unit',
|
||||
title: this.$t('label.quota.type.unit'),
|
||||
dataIndex: 'unit',
|
||||
width: 'calc(100% / 3)'
|
||||
},
|
||||
{
|
||||
key: 'quota',
|
||||
title: this.$t('label.quota.usage'),
|
||||
dataIndex: 'quota',
|
||||
width: 'calc(100% / 3)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tab () {
|
||||
if (this.tab === 'quota.statement.quota') {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
this.loading = true
|
||||
this.dataSource = []
|
||||
this.account = this.$route.query && this.$route.query.account ? this.$route.query.account : null
|
||||
|
||||
try {
|
||||
const resource = await this.quotaBalance()
|
||||
const quotaStatement = await this.quotaStatement(resource)
|
||||
const quotaUsage = quotaStatement.quotausage
|
||||
this.dataSource = this.dataSource.concat(quotaUsage)
|
||||
this.currency = quotaStatement.currency
|
||||
this.totalQuota = quotaStatement.totalquota
|
||||
this.dataSource.push({
|
||||
name: `${this.$t('label.quota.total')}: `,
|
||||
quota: this.totalQuota
|
||||
})
|
||||
this.dataSource.unshift({
|
||||
type: 0,
|
||||
name: `${this.$t('startdate')}: ${moment(this.resource.startdate).format(this.pattern)}`,
|
||||
unit: `${this.$t('enddate')}: ${moment(this.resource.enddate).format(this.pattern)}`
|
||||
})
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
quotaBalance () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {}
|
||||
params.domainid = this.resource.domainid
|
||||
params.account = this.account
|
||||
|
||||
getAPI('quotaBalance', params).then(json => {
|
||||
const quotaBalance = json.quotabalanceresponse.balance || {}
|
||||
resolve(quotaBalance)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
quotaStatement (resource) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {}
|
||||
params.domainid = this.resource.domainid
|
||||
params.account = this.account
|
||||
params.startdate = moment(this.resource.startdate).format(this.pattern)
|
||||
params.enddate = moment(resource.startdate).format(this.pattern)
|
||||
|
||||
getAPI('quotaStatement', params).then(json => {
|
||||
const quotaStatement = json.quotastatementresponse.statement || {}
|
||||
resolve(quotaStatement)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,733 @@
|
|||
// 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>
|
||||
<filter-quota-data-by-period-view @fetchData="fetchData"/>
|
||||
|
||||
<div v-if="dataSource.length > 0">
|
||||
<hr class="m-20-0" />
|
||||
<div class="chart-row">
|
||||
<a-space direction="vertical">
|
||||
<div>
|
||||
<a-radio-group
|
||||
v-model:value="graphType"
|
||||
buttonStyle="solid">
|
||||
<a-radio-button value="bar_chart">
|
||||
{{ $t('label.total') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="line_chart">
|
||||
{{ $t('label.quota.statement.history') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="incremental_chart">
|
||||
{{ $t('label.quota.statement.cumulative.history') }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="font-size: 18px">
|
||||
<strong> {{ $t('label.quota.usage.types.summary') }} </strong>
|
||||
</div>
|
||||
<export-to-csv-button :action="exportDataToCsv" />
|
||||
<bar-chart v-if="graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getUsageTypeBarChartData()"/>
|
||||
<resource-stats-line-chart
|
||||
v-else
|
||||
:chart-labels="usageLineChartLabels"
|
||||
:chart-data="getEntryForCurrentGraphType(this.usageLineChartData)"
|
||||
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.usageTypes))"
|
||||
:yAxisMeasurementUnit="''"
|
||||
/>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource.filter(row => row.quota > 0)"
|
||||
:rowKey="record => record.name"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }">
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.dataIndex === 'name'">
|
||||
<a @click="handleSelectedTypeChange(`${record.type}-${record.name}`)">{{ $t(text) }}</a>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'unit'">
|
||||
{{ $t(text) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'quota'">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>
|
||||
{{ text }}
|
||||
</template>
|
||||
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template #footer >
|
||||
<div style="text-align: right;">
|
||||
{{ $t('label.currency') }}: <b>{{ currency }}</b><br/>
|
||||
{{ $t('label.quota.total.consumption') }}:
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
{{ totalQuota }}
|
||||
</template>
|
||||
<b class="dotted-underline">{{ parseFloat(totalQuota).toFixed(2) }}</b>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<hr class="m-20-0" id="resource-by-type" />
|
||||
<strong>
|
||||
<tooltip-label style="font-size: 18px" :title="$t('label.quota.usage.resources.by.type')" :tooltip="$t('message.quota.usage.resource.warn')"/>
|
||||
</strong>
|
||||
<a-select
|
||||
v-model:value="selectedType"
|
||||
class="w-100"
|
||||
style="margin: 5px 0 10px 0px"
|
||||
show-search
|
||||
@change="handleSelectedTypeChange">
|
||||
<a-select-option
|
||||
v-for="quotaType of getQuotaTypesFiltered()"
|
||||
:value="`${quotaType.id}-${quotaType.type}`"
|
||||
:key="quotaType.id">
|
||||
{{ $t(quotaType.type) }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<export-to-csv-button v-if="dataSourceResource.length > 0" :action="exportResourcesToCsv" :label="`label.export.resources.csv`" />
|
||||
<bar-chart v-if="dataSourceResource.length > 0 && graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getResourceBarChartData()"/>
|
||||
<resource-stats-line-chart
|
||||
v-else-if="dataSourceResource.length > 0 && graphType !== 'bar_chart'"
|
||||
:chart-labels="resourceLineChartLabels"
|
||||
:chart-data="getEntryForCurrentGraphType(this.resourceLineChartData)"
|
||||
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.resources))"
|
||||
:yAxisMeasurementUnit="''"
|
||||
/>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loadingResources"
|
||||
:columns="resourceColumns"
|
||||
:dataSource="dataSourceResource"
|
||||
:rowKey="(record) => record.displayname"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }">
|
||||
<template #title v-if="dataSourceResource.length > 0">
|
||||
<div>{{ $t('label.currency') }}: <b>{{ currency }}</b></div>
|
||||
</template>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.dataIndex === 'displayname'">
|
||||
<span v-if="!text">
|
||||
-
|
||||
</span>
|
||||
<span v-if="text === '<untraceable>' || !record.resourceid">
|
||||
{{ text }}
|
||||
</span>
|
||||
<a v-else @click="handleSelectedResourceChange(record.resourceid)">
|
||||
{{ text }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'quotaconsumed'">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>
|
||||
{{ text }}
|
||||
</template>
|
||||
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<hr class="m-20-0" id="details-by-resource" />
|
||||
<strong>
|
||||
<tooltip-label style="font-size: 18px" :title="$t('label.quota.usage.details.by.resource')"/>
|
||||
</strong>
|
||||
<a-select
|
||||
v-model:value="selectedResource"
|
||||
class="w-100"
|
||||
style="margin: 5px 0 10px 0px"
|
||||
show-search
|
||||
@change="handleSelectedResourceChange"
|
||||
:disabled="getResources().length == 0">
|
||||
<a-select-option
|
||||
v-for="item of getResources()"
|
||||
:value="item.id"
|
||||
:key="item.id">
|
||||
{{ $t(item.name) }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<export-to-csv-button v-if="dataSourceTariffs.length > 0" :action="exportResourceDetailsToCsv" :label="`label.export.details.csv`" />
|
||||
<bar-chart v-if="dataSourceTariffs.length > 0 && graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getTariffsBarChartData()"/>
|
||||
<resource-stats-line-chart
|
||||
v-else-if="dataSourceTariffs.length > 0 && graphType !== 'bar_chart'"
|
||||
:chart-labels="tariffLineChartLabels"
|
||||
:chart-data="getEntryForCurrentGraphType(this.tariffLineChartData)"
|
||||
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.tariffs))"
|
||||
:yAxisMeasurementUnit="''"
|
||||
/>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loadingTariffs"
|
||||
:columns="resourceDetailsColumns"
|
||||
:dataSource="dataSourceTariffs"
|
||||
:rowKey="record => record.tariffname + '-' + record.startdate"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '55vh' }">
|
||||
<template #title v-if="dataSourceTariffs.length > 0">
|
||||
<div>{{ $t('label.currency') }}: <b>{{ currency }}</b></div>
|
||||
</template>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.dataIndex === 'tariffname'">
|
||||
<a v-if="'quotaTariffList' in $store.getters.apis" :href="`#/quotatariff/${record.tariffid}`" target="_blank">
|
||||
{{ text }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'enddate'">
|
||||
{{ $toLocaleDate(text) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'startdate'">
|
||||
{{ $toLocaleDate(text) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'quotaconsumed'">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>
|
||||
{{ text }}
|
||||
</template>
|
||||
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import FilterQuotaDataByPeriodView from './FilterQuotaDataByPeriodView.vue'
|
||||
import BarChart from '@/components/view/charts/BarChart.vue'
|
||||
import ResourceStatsLineChart from '@/components/view/stats/ResourceStatsLineChart.vue'
|
||||
import ExportToCsvButton from '@/components/view/buttons/ExportToCsvButton.vue'
|
||||
import { getChartColorObject } from '@/utils/chart'
|
||||
import { getQuotaTypeByName, getQuotaTypes } from '@/utils/quota'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import * as exportUtils from '@/utils/export'
|
||||
import * as dateUtils from '@/utils/date'
|
||||
|
||||
export default {
|
||||
name: 'QuotaUsageTab',
|
||||
components: {
|
||||
FilterQuotaDataByPeriodView,
|
||||
BarChart,
|
||||
ExportToCsvButton,
|
||||
ResourceStatsLineChart,
|
||||
TooltipLabel
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dataSource: [],
|
||||
selectedType: '',
|
||||
loadingResources: false,
|
||||
dataSourceResource: [],
|
||||
selectedResource: '',
|
||||
loadingTariffs: false,
|
||||
dataSourceTariffs: [],
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
graphType: 'bar_chart',
|
||||
usageLineChartLabels: [],
|
||||
resourceLineChartLabels: [],
|
||||
tariffLineChartLabels: [],
|
||||
usageLineChartData: {},
|
||||
resourceLineChartData: {},
|
||||
tariffLineChartData: {},
|
||||
YAxisMax: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
graphType (newGraphType) {
|
||||
if (newGraphType === 'bar_chart') {
|
||||
return
|
||||
}
|
||||
this.prepareDataForUsageTypeLineGraph()
|
||||
if (!this.selectedType) {
|
||||
return
|
||||
}
|
||||
this.prepareDataForResourceLineGraph()
|
||||
if (!this.selectedResource) {
|
||||
return
|
||||
}
|
||||
this.prepareDataForTariffLineGraph()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return [
|
||||
{
|
||||
title: this.$t('label.quota.type.name'),
|
||||
dataIndex: 'name',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.name.localeCompare(b.name)
|
||||
},
|
||||
{
|
||||
title: this.$t('label.quota.type.unit'),
|
||||
dataIndex: 'unit',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.unit.localeCompare(b.unit)
|
||||
},
|
||||
{
|
||||
title: this.$t('label.quota.consumed'),
|
||||
dataIndex: 'quota',
|
||||
width: 'calc(100% / 3)',
|
||||
sorter: (a, b) => a.quota - b.quota,
|
||||
defaultSortOrder: 'descend'
|
||||
}
|
||||
]
|
||||
},
|
||||
resourceColumns () {
|
||||
return [
|
||||
{
|
||||
title: this.$t('label.resource'),
|
||||
dataIndex: 'displayname',
|
||||
width: '50%',
|
||||
sorter: (a, b) => a.displayname.localeCompare(b.displayname),
|
||||
defaultSortOrder: 'ascend'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.quota.consumed'),
|
||||
dataIndex: 'quotaconsumed',
|
||||
width: '50%',
|
||||
sorter: (a, b) => a.quotaconsumed - b.quotaconsumed
|
||||
}
|
||||
]
|
||||
},
|
||||
resourceDetailsColumns () {
|
||||
return [
|
||||
{
|
||||
title: this.$t('label.quota.tariff'),
|
||||
dataIndex: 'tariffname',
|
||||
sorter: (a, b) => a.tariffname.localeCompare(b.tariffname)
|
||||
},
|
||||
{
|
||||
title: this.$t('label.start.date'),
|
||||
dataIndex: 'startdate',
|
||||
sorter: (a, b) => a.startdate.localeCompare(b.startdate),
|
||||
defaultSortOrder: 'descend'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.end.date'),
|
||||
dataIndex: 'enddate',
|
||||
sorter: (a, b) => a.enddate.localeCompare(b.enddate)
|
||||
},
|
||||
{
|
||||
title: this.$t('label.quota.consumed'),
|
||||
dataIndex: 'quotaconsumed',
|
||||
sorter: (a, b) => a.quotaconsumed - b.quotaconsumed
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchData (startDate, endDate, keepMoment = true) {
|
||||
if (this.loading) return
|
||||
|
||||
this.startDate = dateUtils.parseDayJsObject({ value: startDate, keepMoment: keepMoment })
|
||||
this.endDate = dateUtils.parseDayJsObject({ value: endDate, keepMoment: keepMoment })
|
||||
this.loading = true
|
||||
this.dataSource = []
|
||||
this.dataSourceResource = []
|
||||
this.dataSourceTariffs = []
|
||||
this.selectedResource = ''
|
||||
this.selectedType = ''
|
||||
|
||||
try {
|
||||
const quotaStatement = await this.getQuotaStatement({
|
||||
startdate: this.startDate,
|
||||
enddate: this.endDate
|
||||
})
|
||||
|
||||
if (!quotaStatement) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dataSource = quotaStatement.quotausage.filter(row => row.quota !== 0)
|
||||
if (this.dataSource.length === 0) {
|
||||
this.$notification.info({ message: this.$t('message.request.no.data') })
|
||||
}
|
||||
|
||||
this.currency = quotaStatement.currency
|
||||
this.totalQuota = quotaStatement.totalquota
|
||||
if (this.graphType !== 'bar_chart') {
|
||||
this.prepareDataForUsageTypeLineGraph()
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async fetchResourceData () {
|
||||
if (this.selectedType === '' || this.loadingResources) return
|
||||
|
||||
this.dataSourceResource = []
|
||||
this.loadingResources = true
|
||||
|
||||
try {
|
||||
const quotaStatement = await this.getQuotaStatement({
|
||||
startdate: this.startDate,
|
||||
enddate: this.endDate,
|
||||
showresources: true,
|
||||
type: this.selectedType.split('-')[0]
|
||||
})
|
||||
|
||||
this.dataSourceResource = quotaStatement.quotausage[0].resources.filter(row => row.quotaconsumed !== 0)
|
||||
if (this.dataSourceResource.length === 0) {
|
||||
this.$notification.info({ message: this.$t('message.request.no.data') })
|
||||
}
|
||||
|
||||
if (this.graphType !== 'bar_chart') {
|
||||
this.prepareDataForResourceLineGraph()
|
||||
}
|
||||
} finally {
|
||||
this.loadingResources = false
|
||||
}
|
||||
},
|
||||
async fetchTariffData () {
|
||||
if (this.selectedResource === '' || this.loadingTariffs) return
|
||||
|
||||
this.dataSourceTariffs = []
|
||||
this.loadingTariffs = true
|
||||
|
||||
try {
|
||||
const quotaResourceStatement = await getAPI('quotaResourceStatement', {
|
||||
startdate: this.startDate,
|
||||
enddate: this.endDate,
|
||||
usagetype: this.selectedType.split('-')[0],
|
||||
id: this.selectedResource,
|
||||
accountid: this.$route.params?.id,
|
||||
ignoreproject: true
|
||||
}).then(json => json.quotaresourcestatementresponse?.quotaresourcestatement?.items || [])
|
||||
|
||||
this.dataSourceTariffs = quotaResourceStatement.map(quotaUsage => ({
|
||||
...quotaUsage,
|
||||
startdate: dateUtils.parseDayJsObject({ value: quotaUsage.startdate, keepMoment: false }),
|
||||
enddate: dateUtils.parseDayJsObject({ value: quotaUsage.enddate, keepMoment: false })
|
||||
})).filter(row => row.quotaconsumed !== 0)
|
||||
if (this.dataSourceTariffs.length === 0) {
|
||||
this.$notification.info({ message: this.$t('message.request.no.data') })
|
||||
}
|
||||
|
||||
if (this.graphType !== 'bar_chart') {
|
||||
this.prepareDataForTariffLineGraph()
|
||||
}
|
||||
} finally {
|
||||
this.loadingTariffs = false
|
||||
}
|
||||
},
|
||||
async getQuotaStatement (apiParams) {
|
||||
const params = {
|
||||
ignoreproject: true,
|
||||
accountid: this.$route.params?.id,
|
||||
...apiParams
|
||||
}
|
||||
|
||||
return await getAPI('quotaStatement', params)
|
||||
.then(json => json.quotastatementresponse.statement || {})
|
||||
},
|
||||
getBarChartOptions () {
|
||||
return { responsive: true }
|
||||
},
|
||||
getUsageTypeBarChartData () {
|
||||
const datasets = []
|
||||
for (const row of this.dataSource) {
|
||||
datasets.push({
|
||||
label: this.$t(row.name),
|
||||
data: [row.quota],
|
||||
...this.getColor(row)
|
||||
})
|
||||
}
|
||||
return { labels: [this.$t('label.quota.type.name')], datasets }
|
||||
},
|
||||
getResourceBarChartData () {
|
||||
const datasets = []
|
||||
for (const row of this.dataSourceResource) {
|
||||
datasets.push({
|
||||
label: row.displayname,
|
||||
data: [row.quotaconsumed],
|
||||
...this.getColor(row)
|
||||
})
|
||||
}
|
||||
return { labels: [this.$t('label.resource')], datasets }
|
||||
},
|
||||
getTariffsBarChartData () {
|
||||
const aggregatedTariffs = this.aggregateTariffQuotas()
|
||||
const datasets = []
|
||||
for (const key in aggregatedTariffs) {
|
||||
datasets.push({
|
||||
label: key,
|
||||
data: [aggregatedTariffs[key]],
|
||||
...this.getColor({ tariffname: key })
|
||||
})
|
||||
}
|
||||
return { labels: [this.$t('label.quota.tariff')], datasets }
|
||||
},
|
||||
aggregateTariffQuotas () {
|
||||
const tariffs = {}
|
||||
for (const row of this.dataSourceTariffs) {
|
||||
const currentValue = tariffs[row.tariffname] ?? 0
|
||||
tariffs[row.tariffname] = currentValue + row.quotaconsumed
|
||||
}
|
||||
return tariffs
|
||||
},
|
||||
setUsageTypeLineChartData () {
|
||||
this.usageLineChartLabels = this.getLineChartLabelsForData(this.dataSource)
|
||||
this.usageLineChartData = this.prepareLineChartData(this.dataSource, this.usageLineChartLabels)
|
||||
},
|
||||
setResourceLineChartData () {
|
||||
this.resourceLineChartLabels = this.getLineChartLabelsForData(this.dataSourceResource)
|
||||
this.resourceLineChartData = this.prepareLineChartData(this.dataSourceResource, this.resourceLineChartLabels)
|
||||
},
|
||||
setTariffLineChartData () {
|
||||
this.dataSourceTariffs.sort((a, b) => new Date(a.enddate) - new Date(b.enddate))
|
||||
const usageGroupedByTariffName = this.groupUsageByTariffName()
|
||||
|
||||
const transformedData = Object.values(usageGroupedByTariffName)
|
||||
this.tariffLineChartLabels = this.getLineChartLabelsForData(transformedData)
|
||||
this.tariffLineChartData = this.prepareLineChartData(transformedData, this.tariffLineChartLabels)
|
||||
},
|
||||
groupUsageByTariffName () {
|
||||
const groupedData = {}
|
||||
this.dataSourceTariffs.forEach((obj) => {
|
||||
if (!(obj.tariffname in groupedData)) {
|
||||
groupedData[obj.tariffname] = { tariffname: obj.tariffname, history: [] }
|
||||
}
|
||||
groupedData[obj.tariffname].history.push(obj)
|
||||
})
|
||||
return groupedData
|
||||
},
|
||||
getLineChartLabelForDate (date) {
|
||||
return this.$toLocalDate(date)
|
||||
},
|
||||
getLineChartLabelsForData (data) {
|
||||
const lineChartLabels = [this.getLineChartLabelForDate(this.startDate)]
|
||||
|
||||
for (const row of data) {
|
||||
let isPreviousZero = true
|
||||
for (let i = 0; i < row.history.length; i++) {
|
||||
const item = row.history[i]
|
||||
const isCurrentZero = item.quotaconsumed === 0
|
||||
|
||||
if (isCurrentZero && isPreviousZero) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPreviousZero) {
|
||||
// Last was zero, but we are not. Push our startdate to have an accurate curve
|
||||
this.pushDateToLabelsIfNotPresent(lineChartLabels, this.getLineChartLabelForDate(item.startdate))
|
||||
}
|
||||
|
||||
this.pushDateToLabelsIfNotPresent(lineChartLabels, this.getLineChartLabelForDate(item.enddate))
|
||||
isPreviousZero = isCurrentZero
|
||||
}
|
||||
}
|
||||
|
||||
this.pushDateToLabelsIfNotPresent(lineChartLabels, this.getLineChartLabelForDate(this.endDate))
|
||||
|
||||
lineChartLabels.sort((a, b) => new Date(a) - new Date(b))
|
||||
return lineChartLabels
|
||||
},
|
||||
pushDateToLabelsIfNotPresent (lineChartLabels, date) {
|
||||
const hasDate = lineChartLabels.some(d => {
|
||||
const diff = Math.abs(new Date(date) - new Date(d).getTime())
|
||||
return diff < 5 * 1000 * 60 // Do not push the label if there is already one within 5 minutes
|
||||
})
|
||||
if (!hasDate) {
|
||||
lineChartLabels.push(date)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
calculatePaddedMax (values) {
|
||||
return Math.ceil(Math.max(...values) * 1.2)
|
||||
},
|
||||
getChartDataMaxValues (chartData) {
|
||||
return Object.values(chartData).map(item => Math.max(...item.data.map(obj => obj.stat)))
|
||||
},
|
||||
setYAxisMaxForUsageTypes () {
|
||||
this.YAxisMax.usageTypes = {
|
||||
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.usageLineChartData.incremental)),
|
||||
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.usageLineChartData.history))
|
||||
}
|
||||
},
|
||||
setYAxisMaxForResources () {
|
||||
this.YAxisMax.resources = {
|
||||
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.resourceLineChartData.incremental)),
|
||||
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.resourceLineChartData.history))
|
||||
}
|
||||
},
|
||||
setYAxisMaxForTariffs () {
|
||||
this.YAxisMax.tariffs = {
|
||||
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.tariffLineChartData.incremental)),
|
||||
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.tariffLineChartData.history))
|
||||
}
|
||||
},
|
||||
getEntryForCurrentGraphType (data) {
|
||||
if (this.graphType === 'line_chart') {
|
||||
return data.history
|
||||
}
|
||||
return data.incremental
|
||||
},
|
||||
prepareDataForUsageTypeLineGraph () {
|
||||
this.setUsageTypeLineChartData()
|
||||
this.setYAxisMaxForUsageTypes()
|
||||
},
|
||||
prepareDataForResourceLineGraph () {
|
||||
this.setResourceLineChartData()
|
||||
this.setYAxisMaxForResources()
|
||||
},
|
||||
prepareDataForTariffLineGraph () {
|
||||
this.setTariffLineChartData()
|
||||
this.setYAxisMaxForTariffs()
|
||||
},
|
||||
prepareLineChartData (data, lineChartLabels) {
|
||||
const chartData = { history: [], incremental: [] }
|
||||
|
||||
for (const row of data) {
|
||||
const datasetName = this.$t(this.getName(row))
|
||||
const color = this.getColor(row)
|
||||
|
||||
const historyDataset = { label: datasetName, data: [], fill: false, ...color }
|
||||
const incrementalDataset = { label: datasetName, data: [], fill: false, ...color }
|
||||
|
||||
let historyIndex = 0
|
||||
let accumulatedQuota = 0
|
||||
|
||||
for (const label of lineChartLabels) {
|
||||
const periodQuota = this.calculateQuotaForPeriod(row.history, label, historyIndex)
|
||||
historyIndex = periodQuota.nextIndex
|
||||
|
||||
accumulatedQuota += periodQuota.value
|
||||
historyDataset.data.push({ stat: periodQuota.value })
|
||||
incrementalDataset.data.push({ stat: accumulatedQuota })
|
||||
}
|
||||
|
||||
chartData.history.push(historyDataset)
|
||||
chartData.incremental.push(incrementalDataset)
|
||||
}
|
||||
|
||||
return chartData
|
||||
},
|
||||
calculateQuotaForPeriod (historyItems, label, startIndex) {
|
||||
let quota = 0
|
||||
let currentIndex = startIndex
|
||||
|
||||
while (currentIndex < historyItems.length) {
|
||||
const item = historyItems[currentIndex]
|
||||
if (this.getLineChartLabelForDate(item.enddate) > label) {
|
||||
break
|
||||
}
|
||||
quota += item.quotaconsumed
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
return { value: quota, nextIndex: currentIndex }
|
||||
},
|
||||
getColor (row) {
|
||||
const quotaType = getQuotaTypeByName(row.name)
|
||||
if (quotaType?.chartColor) {
|
||||
return getChartColorObject(quotaType.chartColor)
|
||||
}
|
||||
return getChartColorObject(this.textToDeterministicColor(this.getName(row)))
|
||||
},
|
||||
textToDeterministicColor (text) {
|
||||
let hash = 0
|
||||
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
hash = ((hash << 5) - hash) + text.charCodeAt(i)
|
||||
hash = hash & hash
|
||||
}
|
||||
|
||||
const hash1 = Math.abs(hash)
|
||||
const hash2 = Math.abs(hash * 73)
|
||||
const hash3 = Math.abs(hash * 97)
|
||||
|
||||
let color = '#'
|
||||
color += (hash1 & 0xFF).toString(16).padStart(2, '0')
|
||||
color += (hash2 & 0xFF).toString(16).padStart(2, '0')
|
||||
color += (hash3 & 0xFF).toString(16).padStart(2, '0')
|
||||
|
||||
return color
|
||||
},
|
||||
getName (row) {
|
||||
if (row.name) {
|
||||
// Translate the usage type's name
|
||||
return this.$t(row.name)
|
||||
}
|
||||
return row.displayname || row.tariffname
|
||||
},
|
||||
getYAxisIncrement (max) {
|
||||
if (max < 1) {
|
||||
return 1
|
||||
}
|
||||
return Math.pow(10, Math.floor(Math.log10(max)))
|
||||
},
|
||||
exportDataToCsv () {
|
||||
exportUtils.exportDataToCsv({
|
||||
data: this.dataSource,
|
||||
keys: ['type', 'name', 'unit', 'quota'],
|
||||
fileName: `quota-usage-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
|
||||
})
|
||||
},
|
||||
exportResourcesToCsv () {
|
||||
exportUtils.exportDataToCsv({
|
||||
data: this.dataSourceResource.map(row => ({ ...row, name: row.displayname })),
|
||||
keys: ['resourceid', 'name', 'quotaconsumed'],
|
||||
fileName: `quota-usage-of-resources-of-type-${this.selectedType}-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
|
||||
})
|
||||
},
|
||||
exportResourceDetailsToCsv () {
|
||||
exportUtils.exportDataToCsv({
|
||||
data: this.dataSourceTariffs,
|
||||
keys: ['tariffid', 'tariffname', 'startdate', 'enddate', 'quotaconsumed'],
|
||||
fileName: `detailed-quota-usage-of-resource-${this.selectedResource}-of-type-${this.selectedType}-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
|
||||
})
|
||||
},
|
||||
getQuotaTypesFiltered () {
|
||||
const quotaTypesRetrieved = this.dataSource.map(item => item.name)
|
||||
return getQuotaTypes().filter((item) => quotaTypesRetrieved.includes(item.type))
|
||||
},
|
||||
getResources () {
|
||||
return this.dataSourceResource.filter(item => item.resourceid).map(item => ({ id: item.resourceid, name: item.displayname }))
|
||||
},
|
||||
async handleSelectedTypeChange (value) {
|
||||
this.selectedType = value
|
||||
this.selectedResource = ''
|
||||
this.dataSourceTariffs = []
|
||||
document.getElementById('resource-by-type').scrollIntoView({ behavior: 'smooth' })
|
||||
await this.fetchResourceData()
|
||||
},
|
||||
async handleSelectedResourceChange (value) {
|
||||
this.selectedResource = value
|
||||
document.getElementById('details-by-resource').scrollIntoView({ behavior: 'smooth' })
|
||||
await this.fetchTariffData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/common/common.scss';
|
||||
</style>
|
||||
Loading…
Reference in New Issue