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]

Reply via email to