bernardodemarco commented on code in PR #13449:
URL: https://github.com/apache/cloudstack/pull/13449#discussion_r3539050660
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java:
##########
@@ -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 + "'.")
Review Comment:
```suggestion
description = "ID of the Account for which Quota credits will be
added. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.")
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java:
##########
@@ -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 + "'.")
Review Comment:
```suggestion
description = "ID of the Project for which Quota credits will be
added. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.")
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java:
##########
@@ -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 +
"'.")
Review Comment:
```suggestion
description = "ID of the Account for which the credit statement
will be generated. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.")
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java:
##########
@@ -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. " +
Review Comment:
If it is only available for administrators, couldn't we use the `authorized`
field here?
##########
ui/src/utils/export.js:
##########
@@ -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
Review Comment:
In the `ui/src/utils/util.js` utilities file, we already have a `toCsv`
function. Couldn't we gather together both implementations into a single one?
##########
ui/src/views/plugins/quota/QuotaUsageTab.vue:
##########
@@ -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) {
Review Comment:
```suggestion
if (!hasAnotherDate) {
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java:
##########
@@ -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 +
"'.")
Review Comment:
```suggestion
description = "ID of the Project for which the credit statement
will be generated. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.")
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java:
##########
@@ -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;
Review Comment:
We don't need to explicitly initialize these attributes as `null` right? It
is the default behavior
##########
ui/src/views/plugins/quota/QuotaUsageTab.vue:
##########
@@ -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
Review Comment:
```suggestion
// Last is zero, but current is not. Push our startdate to have
an accurate curve
```
##########
ui/src/views/plugins/quota/AddQuotaCredit.vue:
##########
@@ -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) {
Review Comment:
```suggestion
fetchOwnerOptions (ownerOptions) {
```
##########
ui/src/views/plugins/quota/AddQuotaCredit.vue:
##########
@@ -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
+ }
+ }
+ }
+}
Review Comment:
```suggestion
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
}
}
}
}
```
##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java:
##########
@@ -116,7 +124,10 @@ public void execute() {
@Override
public long getEntityOwnerId() {
- return -1;
+ if (ObjectUtils.allNull(accountId, projectId)) {
+ return -1;
+ }
+ return _accountService.finalizeAccountId(accountId, null, null,
projectId);
Review Comment:
What happens if both `accountId` and `projectId` are specified together?
And, we have access validation to these parameters, right?
##########
ui/src/views/plugins/quota/QuotaUsageTab.vue:
##########
@@ -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 => {
Review Comment:
Maybe renaming this variable to `hasAnotherDate` would be more semantic here:
```suggestion
const hasAnotherDate = lineChartLabels.some(d => {
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]